From 8858f804cca31d9e1c6071ea3df919b7dffd6947 Mon Sep 17 00:00:00 2001
From: labkey-nicka
Date: Fri, 3 Oct 2025 16:07:15 -0700
Subject: [PATCH 1/7] Proof of concept
---
.../labkey/api/data/DataRegionSelection.java | 1094 +-
api/webapp/clientapi/dom/DataRegion.js | 9986 +++++++++--------
2 files changed, 5551 insertions(+), 5529 deletions(-)
diff --git a/api/src/org/labkey/api/data/DataRegionSelection.java b/api/src/org/labkey/api/data/DataRegionSelection.java
index ec2a3c5e1ca..987341e3b1c 100644
--- a/api/src/org/labkey/api/data/DataRegionSelection.java
+++ b/api/src/org/labkey/api/data/DataRegionSelection.java
@@ -1,541 +1,553 @@
-/*
- * Copyright (c) 2008-2019 LabKey Corporation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.labkey.api.data;
-
-import org.apache.commons.lang3.StringUtils;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.labkey.api.action.ApiSimpleResponse;
-import org.labkey.api.collections.LongArrayList;
-import org.labkey.api.collections.ResultSetRowMapFactory;
-import org.labkey.api.miniprofiler.MiniProfiler;
-import org.labkey.api.miniprofiler.Timing;
-import org.labkey.api.query.QueryForm;
-import org.labkey.api.query.QueryService;
-import org.labkey.api.query.QueryView;
-import org.labkey.api.util.Pair;
-import org.labkey.api.util.SessionHelper;
-import org.labkey.api.view.ActionURL;
-import org.labkey.api.view.BadRequestException;
-import org.labkey.api.view.DataView;
-import org.labkey.api.view.NotFoundException;
-import org.labkey.api.view.ViewContext;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpSession;
-import java.io.IOException;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Manages row selection states, scoped to schema/query and possibly a separate selection key.
- * Uses a synchronized Set. As per documentation on {@link Collections#synchronizedSet(Set)}, callers
- * should do their own synchronization on the set itself if they are operating on it one element at a time
- * and want to have a consistent view. This allows for the backing set to be a {@link LinkedHashSet}.
- * User: kevink
- * Date: Jan 3, 2008
- */
-public class DataRegionSelection
-{
- public static final String SELECTED_VALUES = ".selectValues";
- public static final String SEPARATOR = "$";
- public static final String DATA_REGION_SELECTION_KEY = "dataRegionSelectionKey";
-
- // set/updated using query-setSnapshotSelection
- // can be used to hold an arbitrary set of selections in session
- // example usage: set a filtered set of selected values in session
- public static final String SNAPSHOT_SELECTED_VALUES = ".snapshotSelectValues";
-
- private static @NotNull String getSessionAttributeKey(@NotNull String path, @NotNull String key, boolean useSnapshot)
- {
- return path + key + (useSnapshot ? SNAPSHOT_SELECTED_VALUES : SELECTED_VALUES);
- }
-
- private static @NotNull Set getSet(ViewContext context, @Nullable String key, boolean create)
- {
- return getSet(context, key, create, false);
- }
-
- /**
- * * Uses a synchronized Set. As per documentation on {@link Collections#synchronizedSet(Set)}, callers
- * * should do their own synchronization on the set itself if they are operating on it one element at a time
- * * and want to have a consistent view
- */
- private static @NotNull Set getSet(ViewContext context, @Nullable String key, boolean create, boolean useSnapshot)
- {
- if (key == null)
- key = getSelectionKeyFromRequest(context);
-
- if (key != null)
- {
- key = getSessionAttributeKey(context.getContainer().getPath(), key, useSnapshot);
- var request = context.getRequest();
- HttpSession session = request != null ? context.getRequest().getSession(false) : null;
- if (session != null)
- {
- // Ensure that two different requests don't end up creating two different selection sets
- // in the same session
- synchronized (SessionHelper.getSessionLock(session))
- {
- @SuppressWarnings("unchecked") Set result = (Set) session.getAttribute(key);
- if (result == null)
- {
- result = Collections.synchronizedSet(new LinkedHashSet<>());
-
- if (create)
- session.setAttribute(key, result);
- }
- return result;
- }
- }
- }
-
- return Collections.synchronizedSet(new LinkedHashSet<>());
- }
-
- /**
- * Composes a selection key string used to uniquely identify the selected items
- * of a given dataregion. Nulls are allowed.
- */
- public static String getSelectionKey(String schemaName, String queryName, String viewName, String dataRegionName)
- {
- StringBuilder buf = new StringBuilder();
-
- for (String s : new String[]{schemaName, queryName, viewName, dataRegionName})
- {
- buf.append(SEPARATOR);
- if (s != null)
- buf.append(s);
- }
-
- return buf.toString();
- }
-
- /**
- * Get selected items from the request parameters including both current page's selection and session state
- * @return an unmodifiable copy of the selected item ids
- */
- public static @NotNull Set getSelected(ViewContext context)
- {
- return getSelected(context, null, true);
- }
-
- /**
- * Get selected items from the request parameters including both current page's selection and session state
- * @param context Used to get the selection key
- * @param clearSelection Remove the request parameter selected items from session selection state
- * @return an unmodifiable copy of the selected item ids
- */
- public static @NotNull Set getSelected(ViewContext context, boolean clearSelection)
- {
- return getSelected(context, null, clearSelection);
- }
-
- /**
- * Tests if selected items are in the request parameters or session state
- * @param context Used to get the selection key
- * @return true if there are selected item ids, false if not
- */
- public static boolean hasSelected(ViewContext context)
- {
- return !getSelected(context, null, false).isEmpty();
- }
-
- /**
- * Get selected items from the request parameters as integers including both current page's selection and session
- * state and clears the state
- * @param context Used to get the selection key
- * @return an unmodifiable copy of the selected item ids
- */
- public static @NotNull Set getSelectedIntegers(ViewContext context)
- {
- return asLongs(getSelected(context, true));
- }
-
- /**
- * Get selected items from the request parameters as integers including both current page's selection and session state
- * @param context Used to get the selection key
- * @param clearSelection Remove the request parameter selected items from session selection state
- * @return an unmodifiable copy of the selected item ids
- */
- public static @NotNull Set getSelectedIntegers(ViewContext context, boolean clearSelection)
- {
- return asLongs(getSelected(context, null, clearSelection));
- }
-
- @Nullable
- public static String getSelectionKeyFromRequest(ViewContext context)
- {
- HttpServletRequest request = context.getRequest();
- return request == null ? null : request.getParameter(DATA_REGION_SELECTION_KEY);
- }
-
- /**
- * Get the selected items from the request parameters (the current page of a data region) and session state.
- * @param context Contains the session
- * @param key The data region selection key; if null the DATA_REGION_SELECTION_KEY request parameter will be used
- * @param clearSession Remove the request parameter selected items from session selection state
- * @return an unmodifiable copy of the selected item ids
- */
- public static @NotNull Set getSelected(ViewContext context, @Nullable String key, boolean clearSession)
- {
- String[] values = null;
- var request = context.getRequest();
- if (request != null)
- values = context.getRequest().getParameterValues(DataRegion.SELECT_CHECKBOX_NAME);
- if (null != values && values.length == 1 && values[0].contains("\t"))
- values = StringUtils.split(values[0],'\t');
- List parameterSelected = values == null ? new ArrayList<>() : Arrays.asList(values);
- Set result = new LinkedHashSet<>(parameterSelected);
-
- Set sessionSelected = getSet(context, key, false);
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (sessionSelected)
- {
- result.addAll(sessionSelected);
- if (clearSession)
- sessionSelected.removeAll(result);
- }
-
- return Collections.unmodifiableSet(result);
- }
-
- /**
- * Get the selected items from the request parameters (the current page of a data region) and session state as integers.
- */
- public static @NotNull Set getSelectedIntegers(ViewContext context, @Nullable String key, boolean clearSession)
- {
- return asLongs(getSelected(context, key, clearSession));
- }
-
- public static @NotNull ArrayList getSnapshotSelected(ViewContext context, @Nullable String key)
- {
- return new ArrayList<>(getSet(context, key, false, true));
- }
-
- public static @NotNull ArrayList getSnapshotSelectedIntegers(ViewContext context, @Nullable String key)
- {
- return new LongArrayList(asLongs(getSnapshotSelected(context, key)));
- }
-
- private static @NotNull Set asLongs(Collection ids)
- {
- Set result = new LinkedHashSet<>();
- for (String s : ids)
- {
- try
- {
- result.add(Long.parseLong(s));
- }
- catch (NumberFormatException nfe)
- {
- throw new BadRequestException("Unable to convert " + s + " to an int", nfe);
- }
- }
-
- return result;
- }
-
- public static int setSelected(ViewContext context, String key, Collection selection, boolean checked)
- {
- return setSelected(context, key, selection, checked, false);
- }
-
- /**
- * Sets the checked state for the given ids in the session state.
- */
- public static int setSelected(ViewContext context, String key, Collection selection, boolean checked, boolean useSnapshot)
- {
- Set selectedValues = getSet(context, key, true, useSnapshot);
- if (checked)
- selectedValues.addAll(selection);
- else
- selectedValues.removeAll(selection);
- return selectedValues.size();
- }
-
- /**
- * Clear any session attributes that match the given container path, as the prefix, and the selection key, as the suffix
- */
- public static void clearRelatedByContainerPath(ViewContext context, String key)
- {
- if (key == null || context.getRequest() == null)
- return;
-
- HttpSession session = context.getRequest().getSession(false);
- String containerPath = context.getContainer().getPath();
- Collections.list(session.getAttributeNames()).stream()
- .filter(name -> name.startsWith(containerPath) && (name.endsWith(key + SNAPSHOT_SELECTED_VALUES) || name.endsWith(key + SELECTED_VALUES)))
- .forEach(session::removeAttribute);
- }
-
- private static void clearAll(HttpSession session, String path, String key, boolean isSnapshot)
- {
- assert path != null : "DataRegion container path required";
- assert key != null : "DataRegion selection key required";
- if (session == null)
- return;
- session.removeAttribute(getSessionAttributeKey(path, key, isSnapshot));
- }
-
- /**
- * Removes all selection state from the session for RenderContext.getSelectionKey().
- */
- public static void clearAll(RenderContext ctx)
- {
- clearAll(ctx.getRequest().getSession(false),
- ctx.getContainer().getPath(), ctx.getCurrentRegion().getSelectionKey(), false);
- }
-
- /**
- * Removes all selection state from the session for the given key. If key is null, the request parameter DATA_REGION_SELECTION_KEY is used.
- */
- public static void clearAll(ViewContext context, @Nullable String key)
- {
- clearAll(context, key, false);
- }
-
- public static void clearAll(ViewContext context, @Nullable String key, boolean isSnapshot)
- {
- HttpServletRequest request = context.getRequest();
- if (key == null)
- key = getSelectionKeyFromRequest(context);
- if (key != null && request != null)
- clearAll(request.getSession(false),
- context.getContainer().getPath(), key, isSnapshot);
- }
-
- /**
- * Removes all selection state from the session for the key given by request parameter DATA_REGION_SELECTION_KEY.
- */
- public static void clearAll(ViewContext context)
- {
- clearAll(context, null);
- }
-
- /**
- * Gets the ids of the selected items for all items in the given query form's view. That is,
- * not just the items on the current page, but all selected items corresponding to the view's filters.
- */
- public static List getSelected(QueryForm form, boolean clearSelected) throws IOException
- {
- List items;
- var view = getQueryView(form);
-
- var selection = getSet(view.getViewContext(), form.getQuerySettings().getSelectionKey(), true);
- items = getSelectedItems(view, selection);
-
- if (clearSelected && !selection.isEmpty())
- {
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (selection)
- {
- items.forEach(selection::remove);
- }
- }
- return Collections.unmodifiableList(items);
- }
-
- private static Pair getDataRegionContext(QueryView view)
- {
- // Turn off features of QueryView
- view.setPrintView(true);
- view.setShowConfiguredButtons(false);
- view.setShowPagination(false);
- view.setShowPaginationCount(false);
- view.setShowDetailsColumn(false);
- view.setShowUpdateColumn(false);
-
- TableInfo table = view.getTable();
- if (table == null)
- {
- throw new NotFoundException("Could not find table");
- }
-
- DataView v = view.createDataView();
- DataRegion rgn = v.getDataRegion();
-
- // Include all rows. If only selected rows are included, it does not
- // respect filters.
- view.getSettings().setShowRows(ShowRows.ALL);
- view.getSettings().setOffset(Table.NO_OFFSET);
-
- RenderContext rc = v.getRenderContext();
- rc.setViewContext(view.getViewContext());
- rc.setCache(false);
-
- setDataRegionColumnsForSelection(rgn, rc, view, table);
-
- return Pair.of(rgn, rc);
- }
-
- private static @NotNull QueryView getQueryView(QueryForm form) throws NotFoundException
- {
- var schema = form.getSchema();
- if (schema == null)
- throw new NotFoundException();
- return schema.createView(form, null);
- }
-
- public static List getValidatedIds(@NotNull List selection, QueryForm form) throws IOException
- {
- return getSelectedItems(getQueryView(form), selection);
- }
-
- /**
- * Sets the selection for all items in the given query form's view
- */
- public static int setSelectionForAll(QueryForm form, boolean checked) throws IOException
- {
- return setSelectionForAll(getQueryView(form), form.getQuerySettings().getSelectionKey(), checked);
- }
-
- private static void setDataRegionColumnsForSelection(DataRegion rgn, RenderContext rc, QueryView view, TableInfo table)
- {
- // force the pk column(s) into the default list of columns
- List selectorColNames = rgn.getRecordSelectorValueColumns();
- if (selectorColNames == null)
- selectorColNames = table.getPkColumnNames();
- List selectorColumns = new ArrayList<>();
- for (String colName : selectorColNames)
- {
- if (null == rgn.getDisplayColumn(colName)) {
- selectorColumns.add(table.getColumn(colName));
- }
- }
- ActionURL url = view.getSettings().getSortFilterURL();
-
- Sort sort = rc.buildSort(table, url, rgn.getName());
- SimpleFilter filter = rc.buildFilter(table, rc.getColumnInfos(rgn.getDisplayColumns()), url, rgn.getName(), Table.ALL_ROWS, 0, sort);
-
- // Issue 36600: remove unnecessary columns for performance purposes
- rgn.clearColumns();
- // Issue 39011: then add back the columns needed by the filters, if any
- Collection filterColumns = QueryService.get().ensureRequiredColumns(table, selectorColumns, filter, sort, null);
- rgn.addColumns(selectorColumns);
- rgn.addColumns(filterColumns);
- }
-
- public static int setSelectionForAll(QueryView view, String key, boolean checked) throws IOException
- {
- var regionCtx = getDataRegionContext(view);
- var rgn = regionCtx.first;
- var rc = regionCtx.second;
-
- try (Timing ignored = MiniProfiler.step("selectAll"); ResultSet rs = rgn.getResults(rc))
- {
- var selection = createSelectionList(rc, rgn, rs, null);
- return setSelected(view.getViewContext(), key, selection, checked);
- }
- catch (SQLException e)
- {
- throw new RuntimeSQLException(e);
- }
- }
-
- /**
- * Returns all items in the given result set that are selected and selectable
- * @param view the view from which to retrieve the data region context and session variable
- * @param selectedValues optionally (nullable) specify a collection of selected values that will be matched
- * against when selecting items. If null, then all items will be returned.
- * @return list of items from the result set that are in the selected session, or an empty list if none.
- */
- private static List getSelectedItems(QueryView view, @NotNull Collection selectedValues) throws IOException
- {
- // Issue 48657: no need to query the region result set if we have no selectedValues
- if (selectedValues.isEmpty())
- return new LinkedList<>();
-
- var dataRegionContext = getDataRegionContext(view);
- var rgn = dataRegionContext.first;
- var ctx = dataRegionContext.second;
-
- // Issue 48657: no need to query for all region results if we are only interested in a subset, filter for just those we want to verify
- // Note: this only currently applies for tables with a single PK col. Consider altering this for multi-pk tables.
- List pkCols = rgn.getTable().getPkColumns();
- if (pkCols.size() == 1)
- {
- ColumnInfo pkCol = pkCols.get(0);
- ctx.setBaseFilter(new SimpleFilter(pkCol.getFieldKey(), pkCol.isNumericType() ? selectedValues.stream().map(Integer::parseInt).toList() : selectedValues, CompareType.IN));
- }
-
- try (Timing ignored = MiniProfiler.step("getSelected"); Results rs = rgn.getResults(ctx))
- {
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (selectedValues)
- {
- return createSelectionList(ctx, rgn, rs, selectedValues);
- }
- }
- catch (SQLException e)
- {
- throw new RuntimeSQLException(e);
- }
- }
-
- private static List createSelectionList(
- RenderContext ctx,
- DataRegion rgn,
- ResultSet rs,
- @Nullable Collection selectedValues
- ) throws SQLException
- {
- List selected = new LinkedList<>();
-
- if (rs != null)
- {
- ResultSetRowMapFactory factory = ResultSetRowMapFactory.create(rs);
- while (rs.next())
- {
- ctx.setRow(factory.getRowMap(rs));
-
- // Issue 35513: Don't select un-selectables
- if (rgn.isRecordSelectorEnabled(ctx))
- {
- var value = rgn.getRecordSelectorValue(ctx);
- if (selectedValues == null || selectedValues.contains(value))
- selected.add(value);
- }
- }
- }
-
- return selected;
- }
-
- /** Response used from SelectAll, ClearAll, and similar APIs for bulk selecting/unselecting data rows */
- public static class SelectionResponse extends ApiSimpleResponse
- {
- public SelectionResponse(int count)
- {
- super("count", count);
- }
- }
-
- public interface DataSelectionKeyForm
- {
- String getDataRegionSelectionKey();
- void setDataRegionSelectionKey(String key);
- }
-}
+/*
+ * Copyright (c) 2008-2019 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.labkey.api.data;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.labkey.api.action.ApiSimpleResponse;
+import org.labkey.api.collections.LongArrayList;
+import org.labkey.api.collections.ResultSetRowMapFactory;
+import org.labkey.api.miniprofiler.MiniProfiler;
+import org.labkey.api.miniprofiler.Timing;
+import org.labkey.api.query.QueryForm;
+import org.labkey.api.query.QueryService;
+import org.labkey.api.query.QueryView;
+import org.labkey.api.util.Formats;
+import org.labkey.api.util.Pair;
+import org.labkey.api.util.SessionHelper;
+import org.labkey.api.view.ActionURL;
+import org.labkey.api.view.BadRequestException;
+import org.labkey.api.view.DataView;
+import org.labkey.api.view.NotFoundException;
+import org.labkey.api.view.ViewContext;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manages row selection states, scoped to schema/query and possibly a separate selection key.
+ * Uses a synchronized Set. As per documentation on {@link Collections#synchronizedSet(Set)}, callers
+ * should do their own synchronization on the set itself if they are operating on it one element at a time
+ * and want to have a consistent view. This allows for the backing set to be a {@link LinkedHashSet}.
+ */
+public class DataRegionSelection
+{
+ public static final String SELECTED_VALUES = ".selectValues";
+ public static final String SEPARATOR = "$";
+ public static final String DATA_REGION_SELECTION_KEY = "dataRegionSelectionKey";
+ public static final int MAX_SELECTION_SIZE = 1_000;
+
+ // set/updated using query-setSnapshotSelection
+ // can be used to hold an arbitrary set of selections in session
+ // example usage: set a filtered set of selected values in session
+ public static final String SNAPSHOT_SELECTED_VALUES = ".snapshotSelectValues";
+
+ private static @NotNull String getSessionAttributeKey(@NotNull String path, @NotNull String key, boolean useSnapshot)
+ {
+ return path + key + (useSnapshot ? SNAPSHOT_SELECTED_VALUES : SELECTED_VALUES);
+ }
+
+ private static @NotNull Set getSet(ViewContext context, @Nullable String key, boolean create)
+ {
+ return getSet(context, key, create, false);
+ }
+
+ /**
+ * * Uses a synchronized Set. As per documentation on {@link Collections#synchronizedSet(Set)}, callers
+ * * should do their own synchronization on the set itself if they are operating on it one element at a time
+ * * and want to have a consistent view
+ */
+ private static @NotNull Set getSet(ViewContext context, @Nullable String key, boolean create, boolean useSnapshot)
+ {
+ if (key == null)
+ key = getSelectionKeyFromRequest(context);
+
+ if (key != null)
+ {
+ key = getSessionAttributeKey(context.getContainer().getPath(), key, useSnapshot);
+ var request = context.getRequest();
+ HttpSession session = request != null ? context.getRequest().getSession(false) : null;
+ if (session != null)
+ {
+ // Ensure that two different requests don't end up creating two different selection sets
+ // in the same session
+ synchronized (SessionHelper.getSessionLock(session))
+ {
+ @SuppressWarnings("unchecked") Set result = (Set) session.getAttribute(key);
+ if (result == null)
+ {
+ result = Collections.synchronizedSet(new LinkedHashSet<>());
+
+ if (create)
+ session.setAttribute(key, result);
+ }
+ return result;
+ }
+ }
+ }
+
+ return Collections.synchronizedSet(new LinkedHashSet<>());
+ }
+
+ /**
+ * Composes a selection key string used to uniquely identify the selected items
+ * of a given dataregion. Nulls are allowed.
+ */
+ public static String getSelectionKey(String schemaName, String queryName, String viewName, String dataRegionName)
+ {
+ StringBuilder buf = new StringBuilder();
+
+ for (String s : new String[]{schemaName, queryName, viewName, dataRegionName})
+ {
+ buf.append(SEPARATOR);
+ if (s != null)
+ buf.append(s);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Get selected items from the request parameters including both current page's selection and session state
+ * @return an unmodifiable copy of the selected item ids
+ */
+ public static @NotNull Set getSelected(ViewContext context)
+ {
+ return getSelected(context, null, true);
+ }
+
+ /**
+ * Get selected items from the request parameters including both current page's selection and session state
+ * @param context Used to get the selection key
+ * @param clearSelection Remove the request parameter selected items from session selection state
+ * @return an unmodifiable copy of the selected item ids
+ */
+ public static @NotNull Set getSelected(ViewContext context, boolean clearSelection)
+ {
+ return getSelected(context, null, clearSelection);
+ }
+
+ /**
+ * Tests if selected items are in the request parameters or session state
+ * @param context Used to get the selection key
+ * @return true if there are selected item ids, false if not
+ */
+ public static boolean hasSelected(ViewContext context)
+ {
+ return !getSelected(context, null, false).isEmpty();
+ }
+
+ /**
+ * Get selected items from the request parameters as integers including both current page's selection and session
+ * state and clears the state
+ * @param context Used to get the selection key
+ * @return an unmodifiable copy of the selected item ids
+ */
+ public static @NotNull Set getSelectedIntegers(ViewContext context)
+ {
+ return asLongs(getSelected(context, true));
+ }
+
+ /**
+ * Get selected items from the request parameters as integers including both current page's selection and session state
+ * @param context Used to get the selection key
+ * @param clearSelection Remove the request parameter selected items from session selection state
+ * @return an unmodifiable copy of the selected item ids
+ */
+ public static @NotNull Set getSelectedIntegers(ViewContext context, boolean clearSelection)
+ {
+ return asLongs(getSelected(context, null, clearSelection));
+ }
+
+ @Nullable
+ public static String getSelectionKeyFromRequest(ViewContext context)
+ {
+ HttpServletRequest request = context.getRequest();
+ return request == null ? null : request.getParameter(DATA_REGION_SELECTION_KEY);
+ }
+
+ /**
+ * Get the selected items from the request parameters (the current page of a data region) and session state.
+ * @param context Contains the session
+ * @param key The data region selection key; if null the DATA_REGION_SELECTION_KEY request parameter will be used
+ * @param clearSession Remove the request parameter selected items from session selection state
+ * @return an unmodifiable copy of the selected item ids
+ */
+ public static @NotNull Set getSelected(ViewContext context, @Nullable String key, boolean clearSession)
+ {
+ String[] values = null;
+ var request = context.getRequest();
+ if (request != null)
+ values = context.getRequest().getParameterValues(DataRegion.SELECT_CHECKBOX_NAME);
+ if (null != values && values.length == 1 && values[0].contains("\t"))
+ values = StringUtils.split(values[0],'\t');
+ List parameterSelected = values == null ? new ArrayList<>() : Arrays.asList(values);
+ Set result = new LinkedHashSet<>(parameterSelected);
+
+ Set sessionSelected = getSet(context, key, false);
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (sessionSelected)
+ {
+ result.addAll(sessionSelected);
+ if (clearSession)
+ sessionSelected.removeAll(result);
+ }
+
+ return Collections.unmodifiableSet(result);
+ }
+
+ /**
+ * Get the selected items from the request parameters (the current page of a data region) and session state as integers.
+ */
+ public static @NotNull Set getSelectedIntegers(ViewContext context, @Nullable String key, boolean clearSession)
+ {
+ return asLongs(getSelected(context, key, clearSession));
+ }
+
+ public static @NotNull ArrayList getSnapshotSelected(ViewContext context, @Nullable String key)
+ {
+ return new ArrayList<>(getSet(context, key, false, true));
+ }
+
+ public static @NotNull ArrayList getSnapshotSelectedIntegers(ViewContext context, @Nullable String key)
+ {
+ return new LongArrayList(asLongs(getSnapshotSelected(context, key)));
+ }
+
+ private static @NotNull Set asLongs(Collection ids)
+ {
+ Set result = new LinkedHashSet<>();
+ for (String s : ids)
+ {
+ try
+ {
+ result.add(Long.parseLong(s));
+ }
+ catch (NumberFormatException nfe)
+ {
+ throw new BadRequestException("Unable to convert " + s + " to an int", nfe);
+ }
+ }
+
+ return result;
+ }
+
+ public static int setSelected(ViewContext context, String key, Collection selection, boolean checked)
+ {
+ return setSelected(context, key, selection, checked, false);
+ }
+
+ /**
+ * Sets the checked state for the given ids in the session state.
+ */
+ public static int setSelected(ViewContext context, String key, Collection selection, boolean checked, boolean useSnapshot)
+ {
+ if (checked && selection.size() > MAX_SELECTION_SIZE)
+ throw new BadRequestException(String.format("Too many selected items: %s. Maximum number of selected items allowed is %s.", Formats.commaf0.format(selection.size()), Formats.commaf0.format(MAX_SELECTION_SIZE)));
+
+ Set selectedValues = getSet(context, key, true, useSnapshot);
+ if (checked)
+ {
+ // TODO: Need to synchronize on this change and not make it if it is too many
+ selectedValues.addAll(selection);
+ if (selectedValues.size() > MAX_SELECTION_SIZE)
+ throw new BadRequestException(String.format("Too many selected items: %s. Maximum number of selected items allowed is %s.", Formats.commaf0.format(selectedValues.size()), Formats.commaf0.format(MAX_SELECTION_SIZE)));
+ }
+ else
+ selectedValues.removeAll(selection);
+ return selectedValues.size();
+ }
+
+ /**
+ * Clear any session attributes that match the given container path, as the prefix, and the selection key, as the suffix
+ */
+ public static void clearRelatedByContainerPath(ViewContext context, String key)
+ {
+ if (key == null || context.getRequest() == null)
+ return;
+
+ HttpSession session = context.getRequest().getSession(false);
+ String containerPath = context.getContainer().getPath();
+ Collections.list(session.getAttributeNames()).stream()
+ .filter(name -> name.startsWith(containerPath) && (name.endsWith(key + SNAPSHOT_SELECTED_VALUES) || name.endsWith(key + SELECTED_VALUES)))
+ .forEach(session::removeAttribute);
+ }
+
+ private static void clearAll(HttpSession session, String path, String key, boolean isSnapshot)
+ {
+ assert path != null : "DataRegion container path required";
+ assert key != null : "DataRegion selection key required";
+ if (session == null)
+ return;
+ session.removeAttribute(getSessionAttributeKey(path, key, isSnapshot));
+ }
+
+ /**
+ * Removes all selection state from the session for RenderContext.getSelectionKey().
+ */
+ public static void clearAll(RenderContext ctx)
+ {
+ clearAll(ctx.getRequest().getSession(false),
+ ctx.getContainer().getPath(), ctx.getCurrentRegion().getSelectionKey(), false);
+ }
+
+ /**
+ * Removes all selection state from the session for the given key. If key is null, the request parameter DATA_REGION_SELECTION_KEY is used.
+ */
+ public static void clearAll(ViewContext context, @Nullable String key)
+ {
+ clearAll(context, key, false);
+ }
+
+ public static void clearAll(ViewContext context, @Nullable String key, boolean isSnapshot)
+ {
+ HttpServletRequest request = context.getRequest();
+ if (key == null)
+ key = getSelectionKeyFromRequest(context);
+ if (key != null && request != null)
+ clearAll(request.getSession(false),
+ context.getContainer().getPath(), key, isSnapshot);
+ }
+
+ /**
+ * Removes all selection state from the session for the key given by request parameter DATA_REGION_SELECTION_KEY.
+ */
+ public static void clearAll(ViewContext context)
+ {
+ clearAll(context, null);
+ }
+
+ /**
+ * Gets the ids of the selected items for all items in the given query form's view. That is,
+ * not just the items on the current page, but all selected items corresponding to the view's filters.
+ */
+ public static List getSelected(QueryForm form, boolean clearSelected) throws IOException
+ {
+ List items;
+ var view = getQueryView(form);
+
+ var selection = getSet(view.getViewContext(), form.getQuerySettings().getSelectionKey(), true);
+ items = getSelectedItems(view, selection);
+
+ if (clearSelected && !selection.isEmpty())
+ {
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (selection)
+ {
+ items.forEach(selection::remove);
+ }
+ }
+ return Collections.unmodifiableList(items);
+ }
+
+ private static Pair getDataRegionContext(QueryView view)
+ {
+ // Turn off features of QueryView
+ view.setPrintView(true);
+ view.setShowConfiguredButtons(false);
+ view.setShowPagination(false);
+ view.setShowPaginationCount(false);
+ view.setShowDetailsColumn(false);
+ view.setShowUpdateColumn(false);
+
+ TableInfo table = view.getTable();
+ if (table == null)
+ {
+ throw new NotFoundException("Could not find table");
+ }
+
+ DataView v = view.createDataView();
+ DataRegion rgn = v.getDataRegion();
+
+ // Include all rows. If only selected rows are included, it does not
+ // respect filters.
+ view.getSettings().setShowRows(ShowRows.ALL);
+ view.getSettings().setOffset(Table.NO_OFFSET);
+
+ RenderContext rc = v.getRenderContext();
+ rc.setViewContext(view.getViewContext());
+ rc.setCache(false);
+
+ setDataRegionColumnsForSelection(rgn, rc, view, table);
+
+ return Pair.of(rgn, rc);
+ }
+
+ private static @NotNull QueryView getQueryView(QueryForm form) throws NotFoundException
+ {
+ var schema = form.getSchema();
+ if (schema == null)
+ throw new NotFoundException();
+ return schema.createView(form, null);
+ }
+
+ public static List getValidatedIds(@NotNull List selection, QueryForm form) throws IOException
+ {
+ return getSelectedItems(getQueryView(form), selection);
+ }
+
+ /**
+ * Sets the selection for all items in the given query form's view
+ */
+ public static int setSelectionForAll(QueryForm form, boolean checked) throws IOException
+ {
+ return setSelectionForAll(getQueryView(form), form.getQuerySettings().getSelectionKey(), checked);
+ }
+
+ private static void setDataRegionColumnsForSelection(DataRegion rgn, RenderContext rc, QueryView view, TableInfo table)
+ {
+ // force the pk column(s) into the default list of columns
+ List selectorColNames = rgn.getRecordSelectorValueColumns();
+ if (selectorColNames == null)
+ selectorColNames = table.getPkColumnNames();
+ List selectorColumns = new ArrayList<>();
+ for (String colName : selectorColNames)
+ {
+ if (null == rgn.getDisplayColumn(colName)) {
+ selectorColumns.add(table.getColumn(colName));
+ }
+ }
+ ActionURL url = view.getSettings().getSortFilterURL();
+
+ Sort sort = rc.buildSort(table, url, rgn.getName());
+ SimpleFilter filter = rc.buildFilter(table, rc.getColumnInfos(rgn.getDisplayColumns()), url, rgn.getName(), Table.ALL_ROWS, 0, sort);
+
+ // Issue 36600: remove unnecessary columns for performance purposes
+ rgn.clearColumns();
+ // Issue 39011: then add back the columns needed by the filters, if any
+ Collection filterColumns = QueryService.get().ensureRequiredColumns(table, selectorColumns, filter, sort, null);
+ rgn.addColumns(selectorColumns);
+ rgn.addColumns(filterColumns);
+ }
+
+ public static int setSelectionForAll(QueryView view, String key, boolean checked) throws IOException
+ {
+ var regionCtx = getDataRegionContext(view);
+ var rgn = regionCtx.first;
+ var rc = regionCtx.second;
+
+ try (Timing ignored = MiniProfiler.step("selectAll"); ResultSet rs = rgn.getResults(rc))
+ {
+ var selection = createSelectionList(rc, rgn, rs, null);
+ return setSelected(view.getViewContext(), key, selection, checked);
+ }
+ catch (SQLException e)
+ {
+ throw new RuntimeSQLException(e);
+ }
+ }
+
+ /**
+ * Returns all items in the given result set that are selected and selectable
+ * @param view the view from which to retrieve the data region context and session variable
+ * @param selectedValues optionally (nullable) specify a collection of selected values that will be matched
+ * against when selecting items. If null, then all items will be returned.
+ * @return list of items from the result set that are in the selected session, or an empty list if none.
+ */
+ private static List getSelectedItems(QueryView view, @NotNull Collection selectedValues) throws IOException
+ {
+ // Issue 48657: no need to query the region result set if we have no selectedValues
+ if (selectedValues.isEmpty())
+ return new LinkedList<>();
+
+ var dataRegionContext = getDataRegionContext(view);
+ var rgn = dataRegionContext.first;
+ var ctx = dataRegionContext.second;
+
+ // Issue 48657: no need to query for all region results if we are only interested in a subset, filter for just those we want to verify
+ // Note: this only currently applies for tables with a single PK col. Consider altering this for multi-pk tables.
+ List pkCols = rgn.getTable().getPkColumns();
+ if (pkCols.size() == 1)
+ {
+ ColumnInfo pkCol = pkCols.get(0);
+ ctx.setBaseFilter(new SimpleFilter(pkCol.getFieldKey(), pkCol.isNumericType() ? selectedValues.stream().map(Integer::parseInt).toList() : selectedValues, CompareType.IN));
+ }
+
+ try (Timing ignored = MiniProfiler.step("getSelected"); Results rs = rgn.getResults(ctx))
+ {
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (selectedValues)
+ {
+ return createSelectionList(ctx, rgn, rs, selectedValues);
+ }
+ }
+ catch (SQLException e)
+ {
+ throw new RuntimeSQLException(e);
+ }
+ }
+
+ private static List createSelectionList(
+ RenderContext ctx,
+ DataRegion rgn,
+ ResultSet rs,
+ @Nullable Collection selectedValues
+ ) throws SQLException
+ {
+ List selected = new ArrayList<>();
+
+ if (rs != null)
+ {
+ ResultSetRowMapFactory factory = ResultSetRowMapFactory.create(rs);
+ while (rs.next())
+ {
+ ctx.setRow(factory.getRowMap(rs));
+
+ // Issue 35513: Don't select un-selectables
+ if (rgn.isRecordSelectorEnabled(ctx))
+ {
+ var value = rgn.getRecordSelectorValue(ctx);
+ if (selectedValues == null || selectedValues.contains(value))
+ {
+ selected.add(value);
+ if (selected.size() == MAX_SELECTION_SIZE)
+ break;
+ }
+ }
+ }
+ }
+
+ return selected;
+ }
+
+ /** Response used from SelectAll, ClearAll, and similar APIs for bulk selecting/unselecting data rows */
+ public static class SelectionResponse extends ApiSimpleResponse
+ {
+ public SelectionResponse(int count)
+ {
+ super("count", count);
+ }
+ }
+
+ public interface DataSelectionKeyForm
+ {
+ String getDataRegionSelectionKey();
+ void setDataRegionSelectionKey(String key);
+ }
+}
diff --git a/api/webapp/clientapi/dom/DataRegion.js b/api/webapp/clientapi/dom/DataRegion.js
index 28899dd1ac4..76f7145dacf 100644
--- a/api/webapp/clientapi/dom/DataRegion.js
+++ b/api/webapp/clientapi/dom/DataRegion.js
@@ -1,4988 +1,4998 @@
-/*
- * Copyright (c) 2015-2019 LabKey Corporation
- *
- * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
- */
-if (!LABKEY.DataRegions) {
- LABKEY.DataRegions = {};
-}
-
-(function($) {
-
- //
- // CONSTANTS
- //
- // Issue 48715: Limit the number of rows that can be displayed in a data region
- var ALL_ROWS_MAX = 5_000;
- var CUSTOM_VIEW_PANELID = '~~customizeView~~';
- var DEFAULT_TIMEOUT = 30_000;
- var PARAM_PREFIX = '.param.';
- var SORT_ASC = '+';
- var SORT_DESC = '-';
-
- //
- // URL PREFIXES
- //
- var ALL_FILTERS_SKIP_PREFIX = '.~';
- var COLUMNS_PREFIX = '.columns';
- var CONTAINER_FILTER_NAME = '.containerFilterName';
- var MAX_ROWS_PREFIX = '.maxRows';
- var OFFSET_PREFIX = '.offset';
- var REPORTID_PREFIX = '.reportId';
- var SORT_PREFIX = '.sort';
- var SHOW_ROWS_PREFIX = '.showRows';
- var VIEWNAME_PREFIX = '.viewName';
-
- // Issue 33536: These prefixes should match the URL parameter key exactly
- var EXACT_MATCH_PREFIXES = [
- COLUMNS_PREFIX,
- CONTAINER_FILTER_NAME,
- MAX_ROWS_PREFIX,
- OFFSET_PREFIX,
- REPORTID_PREFIX,
- SORT_PREFIX,
- SHOW_ROWS_PREFIX,
- VIEWNAME_PREFIX
- ];
-
- var VALID_LISTENERS = [
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name afterpanelhide
- * @event LABKEY.DataRegion.prototype#hidePanel
- * @description Fires after hiding a visible 'Customize Grid' panel.
- */
- 'afterpanelhide',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name afterpanelshow
- * @event LABKEY.DataRegion.prototype.showPanel
- * @description Fires after showing 'Customize Grid' panel.
- */
- 'afterpanelshow',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforechangeview
- * @event
- * @description Fires before changing grid/view/report.
- * @see LABKEY.DataRegion#changeView
- */
- 'beforechangeview',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforeclearsort
- * @event
- * @description Fires before clearing sort applied to grid.
- * @see LABKEY.DataRegion#clearSort
- */
- 'beforeclearsort',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforemaxrowschange
- * @event
- * @description Fires before change page size.
- * @see LABKEY.DataRegion#setMaxRows
- */
- 'beforemaxrowschange',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforeoffsetchange
- * @event
- * @description Fires before change page number.
- * @see LABKEY.DataRegion#setPageOffset
- */
- 'beforeoffsetchange',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforerefresh
- * @event
- * @description Fires before refresh grid.
- * @see LABKEY.DataRegion#refresh
- */
- 'beforerefresh',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforesetparameters
- * @event
- * @description Fires before setting the parameterized query values for this query.
- * @see LABKEY.DataRegion#setParameters
- */
- 'beforesetparameters',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name beforesortchange
- * @event
- * @description Fires before change sorting on the grid.
- * @see LABKEY.DataRegion#changeSort
- */
- 'beforesortchange',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @member
- * @name render
- * @event
- * @description Fires when data region renders.
- */
- 'render',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name selectchange
- * @event
- * @description Fires when data region selection changes.
- */
- 'selectchange',
- /**
- * @memberOf LABKEY.DataRegion.prototype
- * @name success
- * @event
- * @description Fires when data region loads successfully.
- */
- 'success'];
-
- // TODO: Update constants to not include '.' so mapping can be used easier
- var REQUIRE_NAME_PREFIX = {
- '~': true,
- 'columns': true,
- 'param': true,
- 'reportId': true,
- 'sort': true,
- 'offset': true,
- 'maxRows': true,
- 'showRows': true,
- 'containerFilterName': true,
- 'viewName': true,
- 'disableAnalytics': true
- };
-
- //
- // PRIVATE VARIABLES
- //
- var _paneCache = {};
-
- /**
- * The DataRegion constructor is private - to get a LABKEY.DataRegion object, use LABKEY.DataRegions['dataregionname'].
- * @class LABKEY.DataRegion
- * The DataRegion class allows you to interact with LabKey grids, including querying and modifying selection state, filters, and more.
- * @constructor
- */
- LABKEY.DataRegion = function(config) {
- _init.call(this, config, true);
- };
-
- LABKEY.DataRegion.prototype.toJSON = function() {
- return {
- name: this.name,
- schemaName: this.schemaName,
- queryName: this.queryName,
- viewName: this.viewName,
- offset: this.offset,
- maxRows: this.maxRows,
- messages: this.msgbox.toJSON() // hmm, unsure exactly how this works
- };
- };
-
- /**
- *
- * @param {Object} config
- * @param {Boolean} [applyDefaults=false]
- * @private
- */
- var _init = function(config, applyDefaults) {
-
- // ensure name
- if (!config.dataRegionName) {
- if (!config.name) {
- this.name = LABKEY.Utils.id('aqwp');
- }
- else {
- this.name = config.name;
- }
- }
- else if (!config.name) {
- this.name = config.dataRegionName;
- }
- else {
- this.name = config.name;
- }
-
- if (!this.name) {
- throw '"name" is required to initialize a LABKEY.DataRegion';
- }
-
- // _useQWPDefaults is only used on initial construction
- var isQWP = config._useQWPDefaults === true;
- delete config._useQWPDefaults;
-
- if (config.buttonBar && config.buttonBar.items && LABKEY.Utils.isArray(config.buttonBar.items)) {
- // Be tolerant of the caller passing in undefined items, as pageSize has been removed as an option. Strip
- // them out so they don't cause problems downstream. See Issue 34562.
- config.buttonBar.items = config.buttonBar.items.filter(function (value, index, arr) {
- return value;
- });
- }
-
- var settings;
-
- if (applyDefaults) {
-
- // defensively remove, not allowed to be set
- delete config._userSort;
-
- /**
- * Config Options
- */
- var defaults = {
-
- _allowHeaderLock: isQWP,
-
- _failure: isQWP ? LABKEY.Utils.getOnFailure(config) : undefined,
-
- _success: isQWP ? LABKEY.Utils.getOnSuccess(config) : undefined,
-
- aggregates: undefined,
-
- allowChooseQuery: undefined,
-
- allowChooseView: undefined,
-
- async: isQWP,
-
- bodyClass: undefined,
-
- buttonBar: undefined,
-
- buttonBarPosition: undefined,
-
- chartWizardURL: undefined,
-
- /**
- * All rows visible on the current page.
- */
- complete: false,
-
- /**
- * The currently applied container filter. Note, this is only if it is set on the URL, otherwise
- * the containerFilter could come from the view configuration. Use getContainerFilter()
- * on this object to get the right value.
- */
- containerFilter: undefined,
-
- containerPath: undefined,
-
- /**
- * @deprecated use region.name instead
- */
- dataRegionName: this.name,
-
- detailsURL: undefined,
-
- domId: undefined,
-
- /**
- * The faceted filter pane as been loaded
- * @private
- */
- facetLoaded: false,
-
- filters: undefined,
-
- frame: isQWP ? undefined : 'none',
-
- errorType: 'html',
-
- /**
- * Id of the DataRegion. Same as name property.
- */
- id: this.name,
-
- deleteURL: undefined,
-
- importURL: undefined,
-
- insertURL: undefined,
-
- linkTarget: undefined,
-
- /**
- * Maximum number of rows to be displayed. 0 if the count is not limited. Read-only.
- */
- maxRows: 0,
-
- metadata: undefined,
-
- /**
- * Name of the DataRegion. Should be unique within a given page. Read-only. This will also be used as the id.
- */
- name: this.name,
-
- /**
- * The index of the first row to return from the server (defaults to 0). Use this along with the maxRows config property to request pages of data.
- */
- offset: 0,
-
- parameters: undefined,
-
- /**
- * Name of the query to which this DataRegion is bound. Read-only.
- */
- queryName: '',
-
- disableAnalytics: false,
-
- removeableContainerFilter: undefined,
-
- removeableFilters: undefined,
-
- removeableSort: undefined,
-
- renderTo: undefined,
-
- reportId: undefined,
-
- requestURL: isQWP ? window.location.href : (document.location.search.substring(1) /* strip the ? */ || ''),
-
- returnUrl: isQWP ? window.location.href : undefined,
-
- /**
- * Schema name of the query to which this DataRegion is bound. Read-only.
- */
- schemaName: '',
-
- /**
- * An object to use as the callback function's scope. Defaults to this.
- */
- scope: this,
-
- /**
- * URL to use when selecting all rows in the grid. May be null. Read-only.
- */
- selectAllURL: undefined,
-
- selectedCount: 0,
-
- shadeAlternatingRows: undefined,
-
- showBorders: undefined,
-
- showDeleteButton: undefined,
-
- showDetailsColumn: undefined,
-
- showExportButtons: undefined,
-
- showRStudioButton: undefined,
-
- showImportDataButton: undefined,
-
- showInsertNewButton: undefined,
-
- showPagination: undefined,
-
- showPaginationCount: undefined,
-
- showPaginationCountAsync: false,
-
- showRecordSelectors: false,
-
- showFilterDescription: true,
-
- showReports: undefined,
-
- /**
- * An enum declaring which set of rows to show. all | selected | unselected | paginated
- */
- showRows: 'paginated',
-
- showSurroundingBorder: undefined,
-
- showUpdateColumn: undefined,
-
- /**
- * Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab".
- */
- showViewPanel: undefined,
-
- sort: undefined,
-
- sql: undefined,
-
- /**
- * If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false.
- */
- suppressRenderErrors: false,
-
- /**
- * A timeout for the AJAX call, in milliseconds.
- */
- timeout: undefined,
-
- title: undefined,
-
- titleHref: undefined,
-
- totalRows: undefined, // totalRows isn't available when showing all rows.
-
- updateURL: undefined,
-
- userContainerFilter: undefined, // TODO: Incorporate this with the standard containerFilter
-
- userFilters: {},
-
- /**
- * Name of the custom view to which this DataRegion is bound, may be blank. Read-only.
- */
- viewName: null
- };
-
- settings = $.extend({}, defaults, config);
- }
- else {
- settings = $.extend({}, config);
- }
-
- // if showPaginationCountAsync is set to true, make sure that showPaginationCount is false
- if (settings.showPaginationCountAsync && settings.showPaginationCount) {
- settings.showPaginationCount = false;
- }
-
- // if 'filters' is not specified and 'filterArray' is, use 'filterArray'
- if (!LABKEY.Utils.isArray(settings.filters) && LABKEY.Utils.isArray(config.filterArray)) {
- settings.filters = config.filterArray;
- }
-
- // Any 'key' of this object will not be copied from settings to the region instance
- var blackList = {
- failure: true,
- success: true
- };
-
- for (var s in settings) {
- if (settings.hasOwnProperty(s) && !blackList[s]) {
- this[s] = settings[s];
- }
- }
-
- if (config.renderTo) {
- _convertRenderTo(this, config.renderTo);
- }
-
- if (LABKEY.Utils.isArray(this.removeableFilters)) {
- LABKEY.Filter.appendFilterParams(this.userFilters, this.removeableFilters, this.name);
- delete this.removeableFilters; // they've been applied
- }
-
- // initialize sorting
- if (this._userSort === undefined) {
- this._userSort = _getUserSort(this, true /* asString */);
- }
-
- if (LABKEY.Utils.isString(this.removeableSort)) {
- this._userSort = this.removeableSort + (this._userSort ? this._userSort : '');
- delete this.removeableSort;
- }
-
- this._allowHeaderLock = this.allowHeaderLock === true;
-
- if (!config.messages) {
- this.messages = {};
- }
-
- /**
- * @ignore
- * Non-configurable Options
- */
- this.selectionModified = false;
-
- if (this.panelConfigurations === undefined) {
- this.panelConfigurations = {};
- }
-
- if (isQWP && this.renderTo) {
- _load(this);
- }
- else if (!isQWP) {
- _initContexts.call(this);
- _initMessaging.call(this);
- _initSelection.call(this);
- _initPaging.call(this);
- _initHeaderLocking.call(this);
- _initCustomViews.call(this);
- _initPanes.call(this);
- _initReport.call(this);
- }
- // else the user needs to call render
-
- // bind supported listeners
- if (isQWP) {
- var me = this;
- if (config.listeners) {
- var scope = config.listeners.scope || me;
- $.each(config.listeners, function(event, handler) {
- if ($.inArray(event, VALID_LISTENERS) > -1) {
-
- // support either "event: function" or "event: { fn: function }"
- var callback;
- if ($.isFunction(handler)) {
- callback = handler;
- }
- else if ($.isFunction(handler.fn)) {
- callback = handler.fn;
- }
- else {
- throw 'Unsupported listener configuration: ' + event;
- }
-
- $(me).bind(event, function() {
- callback.apply(scope, $(arguments).slice(1));
- });
- }
- else if (event != 'scope') {
- throw 'Unsupported listener: ' + event;
- }
- });
- }
- }
- };
-
- LABKEY.DataRegion.prototype.destroy = function() {
- // clean-up panel configurations because we preserve this in init
- this.panelConfigurations = {};
-
- // currently a no-op, but should be used to clean-up after ourselves
- this.disableHeaderLock();
- };
-
- /**
- * Refreshes the grid, via AJAX region is in async mode (loaded through a QueryWebPart),
- * and via a page reload otherwise. Can be prevented with a listener
- * on the 'beforerefresh'
- * event.
- */
- LABKEY.DataRegion.prototype.refresh = function() {
- $(this).trigger('beforerefresh', this);
-
- if (this.async) {
- _load(this);
- }
- else {
- window.location.reload();
- }
- };
-
- //
- // Filtering
- //
-
- /**
- * Add a filter to this Data Region.
- * @param {LABKEY.Filter} filter
- * @see LABKEY.DataRegion.addFilter static method.
- */
- LABKEY.DataRegion.prototype.addFilter = function(filter) {
- this.clearSelected({quiet: true});
- _updateFilter(this, filter);
- };
-
- /**
- * Removes all filters from the DataRegion
- */
- LABKEY.DataRegion.prototype.clearAllFilters = function() {
- this.clearSelected({quiet: true});
- if (this.async) {
- this.offset = 0;
- this.userFilters = {};
- }
-
- _removeParameters(this, [ALL_FILTERS_SKIP_PREFIX, OFFSET_PREFIX]);
- };
-
- /**
- * Removes all the filters for a particular field
- * @param {string|FieldKey} fieldKey the name of the field from which all filters should be removed
- */
- LABKEY.DataRegion.prototype.clearFilter = function(fieldKey) {
- this.clearSelected({quiet: true});
- var fk = _resolveFieldKey(this, fieldKey);
-
- if (fk) {
- var columnPrefix = '.' + fk.toString() + '~';
-
- if (this.async) {
- this.offset = 0;
-
- if (this.userFilters) {
- var namePrefix = this.name + columnPrefix,
- me = this;
-
- $.each(this.userFilters, function(name, v) {
- if (name.indexOf(namePrefix) >= 0) {
- delete me.userFilters[name];
- }
- });
- }
- }
-
- _removeParameters(this, [columnPrefix, OFFSET_PREFIX]);
- }
- };
-
- /**
- * Returns an Array of LABKEY.Filter instances applied when creating this DataRegion. These cannot be removed through the UI.
- * @returns {Array} Array of {@link LABKEY.Filter} objects that represent currently applied base filters.
- */
- LABKEY.DataRegion.prototype.getBaseFilters = function() {
- if (this.filters) {
- return this.filters.slice();
- }
-
- return [];
- };
-
- /**
- * Returns the {@link LABKEY.Query.containerFilter} currently applied to the DataRegion. Defaults to LABKEY.Query.containerFilter.current.
- * @returns {String} The container filter currently applied to this DataRegion. Defaults to 'undefined' if a container filter is not specified by the configuration.
- * @see LABKEY.DataRegion#getUserContainerFilter to get the containerFilter value from the URL.
- */
- LABKEY.DataRegion.prototype.getContainerFilter = function() {
- var cf;
-
- if (LABKEY.Utils.isString(this.containerFilter) && this.containerFilter.length > 0) {
- cf = this.containerFilter;
- }
- else if (LABKEY.Utils.isObject(this.view) && LABKEY.Utils.isString(this.view.containerFilter) && this.view.containerFilter.length > 0) {
- cf = this.view.containerFilter;
- }
-
- return cf;
- };
-
- LABKEY.DataRegion.prototype.getDataRegion = function() {
- return this;
- };
-
- /**
- * Returns the user {@link LABKEY.Query.containerFilter} parameter from the URL.
- * @returns {LABKEY.Query.containerFilter} The user container filter.
- */
- LABKEY.DataRegion.prototype.getUserContainerFilter = function() {
- return this.getParameter(this.name + CONTAINER_FILTER_NAME);
- };
-
- /**
- * Returns the user filter from the URL. The filter is represented as an Array of objects of the form:
- *
- *
fieldKey: {String} The field key of the filter.
- *
op: {String} The filter operator (eg. "eq" or "in")
- *
value: {String} Optional value to filter by.
- *
- * @returns {Object} Object representing the user filter.
- * @deprecated 12.2 Use getUserFilterArray instead
- */
- LABKEY.DataRegion.prototype.getUserFilter = function() {
-
- if (LABKEY.devMode) {
- console.warn([
- 'LABKEY.DataRegion.getUserFilter() is deprecated since release 12.2.',
- 'Consider using getUserFilterArray() instead.'
- ].join(' '));
- }
-
- return this.getUserFilterArray().map(function(filter) {
- return {
- fieldKey: filter.getColumnName(),
- op: filter.getFilterType().getURLSuffix(),
- value: filter.getValue()
- };
- });
- };
-
- /**
- * Returns an Array of LABKEY.Filter instances constructed from the URL.
- * @returns {Array} Array of {@link LABKEY.Filter} objects that represent currently applied filters.
- */
- LABKEY.DataRegion.prototype.getUserFilterArray = function() {
- var userFilter = [], me = this;
-
- _getParameters(this).forEach(function(pair) {
- if (pair[0].indexOf(me.name + '.') == 0 && pair[0].indexOf('~') > -1) {
- var tilde = pair[0].indexOf('~');
- var fieldKey = pair[0].substring(me.name.length + 1, tilde);
- var op = pair[0].substring(tilde + 1);
- userFilter.push(LABKEY.Filter.create(fieldKey, pair[1], LABKEY.Filter.getFilterTypeForURLSuffix(op)));
- }
- });
-
- return userFilter;
- };
-
- /**
- * Remove a filter on this DataRegion.
- * @param {LABKEY.Filter} filter
- */
- LABKEY.DataRegion.prototype.removeFilter = function(filter) {
- this.clearSelected({quiet: true});
- if (LABKEY.Utils.isObject(filter) && LABKEY.Utils.isFunction(filter.getColumnName)) {
- _updateFilter(this, null, [this.name + '.' + filter.getColumnName() + '~']);
- }
- };
-
- /**
- * Replace a filter on this Data Region. Optionally, supply another filter to replace for cases when the filter
- * columns don't match exactly.
- * @param {LABKEY.Filter} filter
- * @param {LABKEY.Filter} [filterToReplace]
- */
- LABKEY.DataRegion.prototype.replaceFilter = function(filter, filterToReplace) {
- this.clearSelected({quiet: true});
- var target = filterToReplace ? filterToReplace : filter;
- _updateFilter(this, filter, [this.name + '.' + target.getColumnName() + '~']);
- };
-
- /**
- * @ignore
- * @param filters
- * @param columnNames
- */
- LABKEY.DataRegion.prototype.replaceFilters = function(filters, columnNames) {
- this.clearSelected({quiet: true});
- var filterPrefixes = [],
- filterParams = [],
- me = this;
-
- if (LABKEY.Utils.isArray(filters)) {
- filters.forEach(function(filter) {
- filterPrefixes.push(me.name + '.' + filter.getColumnName() + '~');
- filterParams.push([filter.getURLParameterName(me.name), filter.getURLParameterValue()]);
- });
- }
-
- var fieldKeys = [];
-
- if (LABKEY.Utils.isArray(columnNames)) {
- fieldKeys = fieldKeys.concat(columnNames);
- }
- else if ($.isPlainObject(columnNames) && columnNames.fieldKey) {
- fieldKeys.push(columnNames.fieldKey.toString());
- }
-
- // support fieldKeys (e.g. ["ColumnA", "ColumnA/Sub1"])
- // A special case of fieldKey is "SUBJECT_PREFIX/", used by participant group facet
- if (fieldKeys.length > 0) {
- _getParameters(this).forEach(function(param) {
- var p = param[0];
- if (p.indexOf(me.name + '.') === 0 && p.indexOf('~') > -1) {
- $.each(fieldKeys, function(j, name) {
- var postfix = name && name.length && name[name.length - 1] == '/' ? '' : '~';
- if (p.indexOf(me.name + '.' + name + postfix) > -1) {
- filterPrefixes.push(p);
- }
- });
- }
- });
- }
-
- _setParameters(this, filterParams, [OFFSET_PREFIX].concat($.unique(filterPrefixes)));
- };
-
- /**
- * @private
- * @param filter
- * @param filterMatch
- */
- LABKEY.DataRegion.prototype.replaceFilterMatch = function(filter, filterMatch) {
- this.clearSelected({quiet: true});
- var skips = [], me = this;
-
- _getParameters(this).forEach(function(param) {
- if (param[0].indexOf(me.name + '.') === 0 && param[0].indexOf(filterMatch) > -1) {
- skips.push(param[0]);
- }
- });
-
- _updateFilter(this, filter, skips);
- };
-
- //
- // Selection
- //
-
- /**
- * @private
- */
- var _initSelection = function() {
-
- var me = this,
- form = _getFormSelector(this);
-
- if (form && form.length) {
- // backwards compatibility -- some references use this directly
- // if you're looking to use this internally to the region use _getFormSelector() instead
- this.form = form[0];
- }
-
- if (form && this.showRecordSelectors) {
- _onSelectionChange(this);
- }
-
- // Bind Events
- _getAllRowSelectors(this).on('click', function(evt) {
- evt.stopPropagation();
- me.selectPage.call(me, this.checked);
- });
- _getRowSelectors(this).on('click', function() { me.selectRow.call(me, this); });
-
- // click row highlight
- var rows = form.find('.labkey-data-region > tbody > tr');
- rows.on('click', function(e) {
- if (e.target && e.target.tagName.toLowerCase() === 'td') {
- $(this).siblings('tr').removeClass('lk-row-hl');
- $(this).addClass('lk-row-hl');
- _selClickLock = me;
- }
- });
- rows.on('mouseenter', function() {
- $(this).siblings('tr').removeClass('lk-row-over');
- $(this).addClass('lk-row-over');
- });
- rows.on('mouseleave', function() {
- $(this).removeClass('lk-row-over');
- });
-
- if (!_selDocClick) {
- _selDocClick = $(document).on('click', _onDocumentClick);
- }
- };
-
- var _selClickLock; // lock to prevent removing a row highlight that was just applied
- var _selDocClick; // global (shared across all Data Region instances) click event handler instance
-
- // Issue 32898: Clear row highlights on document click
- var _onDocumentClick = function() {
- if (_selClickLock) {
- var form = _getFormSelector(_selClickLock);
- _selClickLock = undefined;
-
- $('.lk-row-hl').each(function() {
- if (!form.has($(this)).length) {
- $(this).removeClass('lk-row-hl');
- }
- });
- }
- else {
- $('.lk-row-hl').removeClass('lk-row-hl');
- }
- };
-
- /**
- * Clear all selected items for the current DataRegion.
- *
- * @param config A configuration object with the following properties:
- * @param {Function} config.success The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'count' of 0 to indicate an empty selection.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- *
- * @see LABKEY.DataRegion#selectPage
- * @see LABKEY.DataRegion.clearSelected static method.
- */
- LABKEY.DataRegion.prototype.clearSelected = function(config) {
- config = config || {};
- config.selectionKey = this.selectionKey;
- config.scope = config.scope || this;
-
- this.selectedCount = 0;
- if (!config.quiet)
- {
- _onSelectionChange(this);
- }
-
- if (config.selectionKey) {
- LABKEY.DataRegion.clearSelected(config);
- }
-
- if (this.showRows == 'selected') {
- _removeParameters(this, [SHOW_ROWS_PREFIX]);
- }
- else if (this.showRows == 'unselected') {
- // keep "SHOW_ROWS_PREFIX=unselected" parameter
- window.location.reload(true);
- }
- else {
- _toggleAllRows(this, false);
- this.removeMessage('selection');
- }
- };
-
- /**
- * Get selected items on the current page of the DataRegion, based on the current state of the checkboxes in the
- * browser's DOM. Note, if the region is paginated, selected items may exist on other pages which will not be
- * included in the results of this function.
- * @see LABKEY.DataRegion#getSelected
- */
- LABKEY.DataRegion.prototype.getChecked = function() {
- var values = [];
- _getRowSelectors(this).each(function() {
- if (this.checked) {
- values.push(this.value);
- }
- });
- return values;
- };
-
- /**
- * Get all selected items for this DataRegion, as maintained in server-state. This will include rows on any
- * pages of a paginated grid, and may not correspond directly with the state of the checkboxes in the current
- * browser window's DOM if the server-side state has been modified.
- *
- * @param config A configuration object with the following properties:
- * @param {Function} config.success The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'selected' that is an array of the primary keys for the selected rows.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- *
- * @see LABKEY.DataRegion.getSelected static method.
- */
- LABKEY.DataRegion.prototype.getSelected = function(config) {
- if (!this.selectionKey)
- return;
-
- config = config || {};
- config.selectionKey = this.selectionKey;
- LABKEY.DataRegion.getSelected(config);
- };
-
- /**
- * Returns the number of selected rows on the current page of the DataRegion. Selected items may exist on other pages.
- * @returns {Integer} the number of selected rows on the current page of the DataRegion.
- * @see LABKEY.DataRegion#getSelected to get all selected rows.
- */
- LABKEY.DataRegion.prototype.getSelectionCount = function() {
- if (!$('#' + this.domId)) {
- return 0;
- }
-
- var count = 0;
- _getRowSelectors(this).each(function() {
- if (this.checked === true) {
- count++;
- }
- });
-
- return count;
- };
-
- /**
- * Returns true if any row is checked on the current page of the DataRegion. Selected items may exist on other pages.
- * @returns {Boolean} true if any row is checked on the current page of the DataRegion.
- * @see LABKEY.DataRegion#getSelected to get all selected rows.
- */
- LABKEY.DataRegion.prototype.hasSelected = function() {
- return this.getSelectionCount() > 0;
- };
-
- /**
- * Returns true if all rows are checked on the current page of the DataRegion and at least one row is present.
- * @returns {Boolean} true if all rows are checked on the current page of the DataRegion and at least one row is present.
- * @see LABKEY.DataRegion#getSelected to get all selected rows.
- */
- LABKEY.DataRegion.prototype.isPageSelected = function() {
- var checkboxes = _getRowSelectors(this);
- var i=0;
-
- for (; i < checkboxes.length; i++) {
- if (!checkboxes[i].checked) {
- return false;
- }
- }
- return i > 0;
- };
-
- LABKEY.DataRegion.prototype.selectAll = function(config) {
- if (this.selectionKey) {
- config = config || {};
- config.scope = config.scope || this;
-
- // Either use the selectAllURL provided or create a query config
- // object that can be used with the generic query/selectAll.api action.
- if (this.selectAllURL) {
- config.url = this.selectAllURL;
- }
- else {
- config = LABKEY.Utils.apply(config, this.getQueryConfig());
- }
-
- config = _chainSelectionCountCallback(this, config);
-
- LABKEY.DataRegion.selectAll(config);
-
- if (this.showRows === "selected") {
- // keep "SHOW_ROWS_PREFIX=selected" parameter
- window.location.reload(true);
- }
- else if (this.showRows === "unselected") {
- _removeParameters(this, [SHOW_ROWS_PREFIX]);
- }
- else {
- _toggleAllRows(this, true);
- }
- }
- };
-
- /**
- * @deprecated use clearSelected instead
- * @function
- * @see LABKEY.DataRegion#clearSelected
- */
- LABKEY.DataRegion.prototype.selectNone = LABKEY.DataRegion.prototype.clearSelected;
-
- /**
- * Set the selection state for all checkboxes on the current page of the DataRegion.
- * @param checked whether all of the rows on the current page should be selected or unselected
- * @returns {Array} Array of ids that were selected or unselected.
- *
- * @see LABKEY.DataRegion#setSelected to set selected items on the current page of the DataRegion.
- * @see LABKEY.DataRegion#clearSelected to clear all selected.
- */
- LABKEY.DataRegion.prototype.selectPage = function(checked) {
- var _check = (checked === true);
- var ids = _toggleAllRows(this, _check);
- var me = this;
-
- if (ids.length > 0) {
- _getAllRowSelectors(this).each(function() { this.checked = _check});
- this.setSelected({
- ids: ids,
- checked: _check,
- success: function(data) {
- if (data && data.count > 0 && !this.complete) {
- var count = data.count;
- var msg;
- if (me.totalRows) {
- if (count == me.totalRows) {
- msg = 'All ' + this.totalRows + ' rows selected.';
- }
- else {
- msg = 'Selected ' + count + ' of ' + this.totalRows + ' rows.';
- }
- }
- else {
- // totalRows isn't available when showing all rows.
- msg = 'Selected ' + count + ' rows.';
- }
- _showSelectMessage(me, msg);
- }
- else {
- this.removeMessage('selection');
- }
- }
- });
- }
-
- return ids;
- };
-
- /**
- * @ignore
- * @param el
- */
- LABKEY.DataRegion.prototype.selectRow = function(el) {
- this.setSelected({
- ids: [el.value],
- checked: el.checked
- });
-
- if (!el.checked) {
- this.removeMessage('selection');
- }
- };
-
- /**
- * Add or remove items from the selection associated with the this DataRegion.
- *
- * @param config A configuration object with the following properties:
- * @param {Array} config.ids Array of primary key ids for each row to select/unselect.
- * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected.
- * @param {Function} [config.success] The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'count' to indicate the updated selection count.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- *
- * @see LABKEY.DataRegion#getSelected to get the selected items for this DataRegion.
- * @see LABKEY.DataRegion#clearSelected to clear all selected items for this DataRegion.
- */
- LABKEY.DataRegion.prototype.setSelected = function(config) {
- if (!config || !LABKEY.Utils.isArray(config.ids) || config.ids.length === 0) {
- return;
- }
-
- var me = this;
- config = config || {};
- config.selectionKey = this.selectionKey;
- config.scope = config.scope || me;
-
- config = _chainSelectionCountCallback(this, config);
-
- var failure = LABKEY.Utils.getOnFailure(config);
- if ($.isFunction(failure)) {
- config.failure = failure;
- }
- else {
- config.failure = function() { me.addMessage('Error sending selection.'); };
- }
-
- if (config.selectionKey) {
- LABKEY.DataRegion.setSelected(config);
- }
- else if ($.isFunction(config.success)) {
- // Don't send the selection change to the server if there is no selectionKey.
- // Call the success callback directly.
- config.success.call(config.scope, {count: this.getSelectionCount()});
- }
- };
-
- //
- // Parameters
- //
-
- /**
- * Removes all parameters from the DataRegion
- */
- LABKEY.DataRegion.prototype.clearAllParameters = function() {
- if (this.async) {
- this.offset = 0;
- this.parameters = undefined;
- }
-
- _removeParameters(this, [PARAM_PREFIX, OFFSET_PREFIX]);
- };
-
- /**
- * Returns the specified parameter from the URL. Note, this is not related specifically
- * to parameterized query values (e.g. setParameters()/getParameters())
- * @param {String} paramName
- * @returns {*}
- */
- LABKEY.DataRegion.prototype.getParameter = function(paramName) {
- var param = null;
-
- $.each(_getParameters(this), function(i, pair) {
- if (pair.length > 0 && pair[0] === paramName) {
- param = pair.length > 1 ? pair[1] : '';
- return false;
- }
- });
-
- return param;
- };
-
- /**
- * Get the parameterized query values for this query. These parameters
- * are named by the query itself.
- * @param {boolean} toLowercase If true, all parameter names will be converted to lowercase
- * returns params An Object of key/val pairs.
- */
- LABKEY.DataRegion.prototype.getParameters = function(toLowercase) {
-
- var params = this.parameters ? this.parameters : {},
- re = new RegExp('^' + LABKEY.Utils.escapeRe(this.name) + LABKEY.Utils.escapeRe(PARAM_PREFIX), 'i'),
- name;
-
- _getParameters(this).forEach(function(pair) {
- if (pair.length > 0 && pair[0].match(re)) {
- name = pair[0].replace(re, '');
- if (toLowercase === true) {
- name = name.toLowerCase();
- }
-
- // URL parameters will override this.parameters values
- params[name] = pair[1];
- }
- });
-
- return params;
- };
-
- /**
- * Set the parameterized query values for this query. These parameters
- * are named by the query itself.
- * @param {Mixed} params An Object or Array of Array key/val pairs.
- */
- LABKEY.DataRegion.prototype.setParameters = function(params) {
- var event = $.Event('beforesetparameters');
-
- $(this).trigger(event);
-
- if (event.isDefaultPrevented()) {
- return;
- }
-
- var paramPrefix = this.name + PARAM_PREFIX, _params = [];
- var newParameters = this.parameters ? this.parameters : {};
-
- function applyParameters(pKey, pValue) {
- var key = pKey;
- if (pKey.indexOf(paramPrefix) !== 0) {
- key = paramPrefix + pKey;
- }
- newParameters[key.replace(paramPrefix, '')] = pValue;
- _params.push([key, pValue]);
- }
-
- // convert Object into Array of Array pairs and prefix the parameter name if necessary.
- if (LABKEY.Utils.isObject(params)) {
- $.each(params, applyParameters);
- }
- else if (LABKEY.Utils.isArray(params)) {
- params.forEach(function(pair) {
- if (LABKEY.Utils.isArray(pair) && pair.length > 1) {
- applyParameters(pair[0], pair[1]);
- }
- });
- }
- else {
- return; // invalid argument shape
- }
-
- this.parameters = newParameters;
-
- _setParameters(this, _params, [PARAM_PREFIX, OFFSET_PREFIX]);
- };
-
- /**
- * @ignore
- * @Deprecated
- */
- LABKEY.DataRegion.prototype.setSearchString = function(regionName, search) {
- this.savedSearchString = search || "";
- // If the search string doesn't change and there is a hash on the url, the page won't reload.
- // Remove the hash by setting the full path plus search string.
- window.location.assign(window.location.pathname + (this.savedSearchString.length > 0 ? "?" + this.savedSearchString : ""));
- };
-
- //
- // Messaging
- //
-
- /**
- * @private
- */
- var _initMessaging = function() {
- if (!this.msgbox) {
- this.msgbox = new MessageArea(this);
- this.msgbox.on('rendermsg', function(evt, msgArea, parts) { _onRenderMessageArea(this, parts); }, this);
- }
- else {
- this.msgbox.bindRegion(this);
- }
-
- if (this.messages) {
- this.msgbox.setMessages(this.messages);
- this.msgbox.render();
- }
- };
-
- /**
- * Show a message in the header of this DataRegion.
- * @param {String / Object} config the HTML source of the message to be shown or a config object with the following properties:
- *
- *
html: {String} the HTML source of the message to be shown.
- *
part: {String} The part of the message area to render the message to.
- *
duration: {Integer} The amount of time (in milliseconds) the message will stay visible.
- *
hideButtonPanel: {Boolean} If true the button panel (customize view, export, etc.) will be hidden if visible.
- *
append: {Boolean} If true the msg is appended to any existing content for the given part.
- *
- * @param part The part of the message area to render the message to. Used to scope messages so they can be added
- * and removed without clearing other messages.
- */
- LABKEY.DataRegion.prototype.addMessage = function(config, part) {
- this.hidePanel();
-
- if (LABKEY.Utils.isString(config)) {
- this.msgbox.addMessage(config, part);
- }
- else if (LABKEY.Utils.isObject(config)) {
- this.msgbox.addMessage(config.html, config.part || part, config.append);
-
- if (config.hideButtonPanel) {
- this.hideButtonPanel();
- }
-
- if (config.duration) {
- var dr = this;
- setTimeout(function() {
- dr.removeMessage(config.part || part);
- _getHeaderSelector(dr).trigger('resize');
- }, config.duration);
- }
- }
- };
-
- /**
- * Clear the message box contents.
- */
- LABKEY.DataRegion.prototype.clearMessage = function() {
- if (this.msgbox) this.msgbox.removeAll();
- };
-
- /**
- * @param part The part of the message area to render the message to. Used to scope messages so they can be added
- * and removed without clearing other messages.
- * @return {String} The message for 'part'. Could be undefined.
- */
- LABKEY.DataRegion.prototype.getMessage = function(part) {
- if (this.msgbox) { return this.msgbox.getMessage(part); } // else undefined
- };
-
- /**
- * @param part The part of the message area to render the message to. Used to scope messages so they can be added
- * and removed without clearing other messages.
- * @return {Boolean} true iff there is a message area for this region and it has the message keyed by 'part'.
- */
- LABKEY.DataRegion.prototype.hasMessage = function(part) {
- return this.msgbox && this.msgbox.hasMessage(part);
- };
-
- LABKEY.DataRegion.prototype.hideContext = function() {
- _getContextBarSelector(this).hide();
- _getViewBarSelector(this).hide();
- };
-
- /**
- * If a message is currently showing, hide it and clear out its contents
- * @param keepContent If true don't remove the message area content
- */
- LABKEY.DataRegion.prototype.hideMessage = function(keepContent) {
- if (this.msgbox) {
- this.msgbox.hide();
-
- if (!keepContent)
- this.removeAllMessages();
- }
- };
-
- /**
- * Returns true if a message is currently being shown for this DataRegion. Messages are shown as a header.
- * @return {Boolean} true if a message is showing.
- */
- LABKEY.DataRegion.prototype.isMessageShowing = function() {
- return this.msgbox && this.msgbox.isVisible();
- };
-
- /**
- * Removes all messages from this Data Region.
- */
- LABKEY.DataRegion.prototype.removeAllMessages = function() {
- if (this.msgbox) { this.msgbox.removeAll(); }
- };
-
- /**
- * If a message is currently showing, remove the specified part
- */
- LABKEY.DataRegion.prototype.removeMessage = function(part) {
- if (this.msgbox) { this.msgbox.removeMessage(part); }
- };
-
- /**
- * Show a message in the header of this DataRegion with a loading indicator.
- * @param html the HTML source of the message to be shown
- */
- LABKEY.DataRegion.prototype.showLoadingMessage = function(html) {
- html = html || "Loading...";
- this.addMessage('
' + html + '
', 'drloading');
- };
-
- LABKEY.DataRegion.prototype.hideLoadingMessage = function() {
- this.removeMessage('drloading');
- };
-
- /**
- * Show a success message in the header of this DataRegion.
- * @param html the HTML source of the message to be shown
- */
- LABKEY.DataRegion.prototype.showSuccessMessage = function(html) {
- html = html || "Completed successfully.";
- this.addMessage('
' + html + '
');
- };
-
- /**
- * Show an error message in the header of this DataRegion.
- * @param html the HTML source of the message to be shown
- */
- LABKEY.DataRegion.prototype.showErrorMessage = function(html) {
- html = html || "An error occurred.";
- this.addMessage('
' + html + '
');
- };
-
- LABKEY.DataRegion.prototype.showContext = function() {
- _initContexts();
-
- var contexts = [
- _getContextBarSelector(this),
- _getViewBarSelector(this)
- ];
-
- for (var i = 0; i < contexts.length; i++) {
- var ctx = contexts[i];
- var html = ctx.html();
-
- if (html && html.trim() !== '') {
- ctx.show();
- }
- }
- };
-
- /**
- * Show a message in the header of this DataRegion.
- * @param msg the HTML source of the message to be shown
- * @deprecated use addMessage(msg, part) instead.
- */
- LABKEY.DataRegion.prototype.showMessage = function(msg) {
- if (this.msgbox) {
- this.msgbox.addMessage(msg);
- }
- };
-
- LABKEY.DataRegion.prototype.showMessageArea = function() {
- if (this.msgbox && this.msgbox.hasContent()) {
- this.msgbox.show();
- }
- };
-
- //
- // Sections
- //
-
- LABKEY.DataRegion.prototype.displaySection = function(options) {
- var dir = options && options.dir ? options.dir : 'n';
-
- var sec = _getSectionSelector(this, dir);
- if (options && options.html) {
- options.append === true ? sec.append(options.html) : sec.html(options.html);
- }
- sec.show();
- };
-
- LABKEY.DataRegion.prototype.hideSection = function(options) {
- var dir = options && options.dir ? options.dir : 'n';
- var sec = _getSectionSelector(this, dir);
-
- sec.hide();
-
- if (options && options.clear === true) {
- sec.html('');
- }
- };
-
- LABKEY.DataRegion.prototype.writeSection = function(content, options) {
- var append = options && options.append === true;
- var dir = options && options.dir ? options.dir : 'n';
-
- var sec = _getSectionSelector(this, dir);
- append ? sec.append(content) : sec.html(content);
- };
-
- //
- // Sorting
- //
-
- /**
- * Replaces the sort on the given column, if present, or sets a brand new sort
- * @param {string or LABKEY.FieldKey} fieldKey name of the column to be sorted
- * @param {string} [sortDir=+] Set to '+' for ascending or '-' for descending
- */
- LABKEY.DataRegion.prototype.changeSort = function(fieldKey, sortDir) {
- if (!fieldKey)
- return;
-
- fieldKey = _resolveFieldKey(this, fieldKey);
-
- var columnName = fieldKey.toString();
-
- var event = $.Event("beforesortchange");
-
- $(this).trigger(event, [this, columnName, sortDir]);
-
- if (event.isDefaultPrevented()) {
- return;
- }
-
- // no need to re-query for totalRowCount, if async
- this.skipTotalRowCount = true;
-
- this._userSort = _alterSortString(this, this._userSort, fieldKey, sortDir);
- _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]);
- };
-
- /**
- * Removes the sort on a specified column
- * @param {string or LABKEY.FieldKey} fieldKey name of the column
- */
- LABKEY.DataRegion.prototype.clearSort = function(fieldKey) {
- if (!fieldKey)
- return;
-
- fieldKey = _resolveFieldKey(this, fieldKey);
-
- var columnName = fieldKey.toString();
-
- var event = $.Event("beforeclearsort");
-
- $(this).trigger(event, [this, columnName]);
-
- if (event.isDefaultPrevented()) {
- return;
- }
-
- // no need to re-query for totalRowCount, if async
- this.skipTotalRowCount = true;
-
- this._userSort = _alterSortString(this, this._userSort, fieldKey);
- if (this._userSort.length > 0) {
- _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]);
- }
- else {
- _removeParameters(this, [SORT_PREFIX, OFFSET_PREFIX]);
- }
- };
-
- /**
- * Returns the user sort from the URL. The sort is represented as an Array of objects of the form:
- *
- *
fieldKey: {String} The field key of the sort.
- *
dir: {String} The sort direction, either "+" or "-".
- *
- * @returns {Object} Object representing the user sort.
- */
- LABKEY.DataRegion.prototype.getUserSort = function() {
- return _getUserSort(this);
- };
-
- //
- // Paging
- //
-
- var _initPaging = function() {
- if (this.showPagination) {
- // Issue 51036: load totalRows count async for DataRegions
- if (!this.complete && this.showPaginationCountAsync && !this.skipTotalRowCount && this.loadingTotalRows === undefined) {
- var params = _getAsyncParams(this, _getParameters(this), false);
- var jsonData = _getAsyncBody(this, params);
- _loadAsyncTotalRowCount(this, params, jsonData);
- }
-
- var ct = _getBarSelector(this).find('.labkey-pagination');
-
- if (ct && ct.length) {
- var hasOffset = $.isNumeric(this.offset);
- var hasTotal = $.isNumeric(this.totalRows);
-
- // display the counts
- if (hasOffset) {
-
- // small result set
- if (hasTotal && this.totalRows < 5) {
- return;
- }
-
- var low = this.offset + 1;
- var high = this.offset + this.rowCount;
-
- // user has opted to show all rows
- if (hasTotal && (this.rowCount === null || this.rowCount < 1)) {
- high = this.totalRows;
- }
-
- var showFirst = _showFirstEnabled(this);
- var showLast = _showLastEnabled(this);
- var showAll = _showAllEnabled(this);
- this.showFirstID = LABKEY.Utils.id();
- this.showLastID = LABKEY.Utils.id();
- this.showAllID = LABKEY.Utils.id();
-
- // If modifying this ensure it is consistent with DOM generated by PopupMenu.java
- var elems = [
- '
'
- ]);
- }
-
- if (html.length) {
- ctxBar.append(html.join(''));
- ctxBar.find('.ctx-clear-var').off('click').on('click', $.proxy(this.clearAllParameters, this));
- ctxBar.find('.ctx-clear-all').off('click').on('click', $.proxy(this.clearAllFilters, this));
- }
-
- // Issue 35396: Support ButtonBarOptions
- if (LABKEY.Utils.isArray(this.buttonBarOnRenders)) {
- for (var i=0; i < this.buttonBarOnRenders.length; i++) {
- var scriptFnName = this.buttonBarOnRenders[i];
- var fnParts = scriptFnName.split('.');
- var scope = window;
- var called = false;
-
- for (var j=0; j < fnParts.length; j++) {
- scope = scope[fnParts[j]];
- if (!scope) break;
- if (j === fnParts.length - 1 && LABKEY.Utils.isFunction(scope)) {
- scope(this);
- called = true;
- }
- }
-
- if (!called) {
- console.warn('Unable to call "' + scriptFnName + '" for DataRegion.ButtonBar.onRender.');
- }
- }
- }
- };
-
- //
- // Customize View
- //
- var _initCustomViews = function() {
- if (this.view && this.view.session) {
- // clear old contents
- _getViewBarSelector(this).find('.labkey-button-bar').remove();
-
- _getViewBarSelector(this).append([
- '
',
- 'This grid view has been modified.',
- 'Revert',
- 'Edit',
- 'Save',
- '
'
- ].join(''));
- _getViewBarSelector(this).find('.unsavedview-revert').off('click').on('click', $.proxy(function() {
- _revertCustomView(this);
- }, this));
- _getViewBarSelector(this).find('.unsavedview-edit').off('click').on('click', $.proxy(function() {
- this.showCustomizeView(undefined);
- }, this));
- _getViewBarSelector(this).find('.unsavedview-save').off('click').on('click', $.proxy(function() {
- _saveSessionCustomView(this);
- }, this));
- }
- };
-
- /**
- * Change the currently selected view to the named view
- * @param {Object} view An object which contains the following properties.
- * @param {String} [view.type] the type of view, either a 'view' or a 'report'.
- * @param {String} [view.viewName] If the type is 'view', then the name of the view.
- * @param {String} [view.reportId] If the type is 'report', then the report id.
- * @param {Object} urlParameters NOTE: Experimental parameter; may change without warning. A set of filter and sorts to apply as URL parameters when changing the view.
- */
- LABKEY.DataRegion.prototype.changeView = function(view, urlParameters) {
- var event = $.Event('beforechangeview');
- $(this).trigger(event, [this, view, urlParameters]);
- if (event.isDefaultPrevented()) {
- return;
- }
-
- var paramValPairs = [],
- newSort = [],
- skipPrefixes = [OFFSET_PREFIX, SHOW_ROWS_PREFIX, VIEWNAME_PREFIX, REPORTID_PREFIX];
-
- // clear sibling parameters
- this.viewName = undefined;
- this.reportId = undefined;
-
- if (view) {
- if (LABKEY.Utils.isString(view)) {
- paramValPairs.push([VIEWNAME_PREFIX, view]);
- this.viewName = view;
- }
- else if (view.type === 'report') {
- paramValPairs.push([REPORTID_PREFIX, view.reportId]);
- this.reportId = view.reportId;
- }
- else if (view.type === 'view' && view.viewName) {
- paramValPairs.push([VIEWNAME_PREFIX, view.viewName]);
- this.viewName = view.viewName;
- }
- }
-
- if (urlParameters) {
- $.each(urlParameters.filter, function(i, filter) {
- paramValPairs.push(['.' + filter.fieldKey + '~' + filter.op, filter.value]);
- });
-
- if (urlParameters.sort && urlParameters.sort.length > 0) {
- $.each(urlParameters.sort, function(i, sort) {
- newSort.push((sort.dir === '+' ? '' : sort.dir) + sort.fieldKey);
- });
- paramValPairs.push([SORT_PREFIX, newSort.join(',')]);
- }
-
- if (urlParameters.containerFilter) {
- paramValPairs.push([CONTAINER_FILTER_NAME, urlParameters.containerFilter]);
- }
-
- // removes all filter, sort, and container filter parameters
- skipPrefixes = skipPrefixes.concat([
- ALL_FILTERS_SKIP_PREFIX, SORT_PREFIX, COLUMNS_PREFIX, CONTAINER_FILTER_NAME
- ]);
- }
-
- // removes all filter, sort, and container filter parameters
- _setParameters(this, paramValPairs, skipPrefixes);
- };
-
- LABKEY.DataRegion.prototype.getQueryDetails = function(success, failure, scope) {
-
- var userSort = this.getUserSort(),
- userColumns = this.getParameter(this.name + COLUMNS_PREFIX),
- fields = [],
- viewName = (this.view && this.view.name) || this.viewName || '';
-
- var userFilter = this.getUserFilterArray().map(function(filter) {
- var fieldKey = filter.getColumnName();
- fields.push(fieldKey);
-
- return {
- fieldKey: fieldKey,
- op: filter.getFilterType().getURLSuffix(),
- value: filter.getValue()
- };
- });
-
- userSort.forEach(function(sort) {
- fields.push(sort.fieldKey);
- });
-
- LABKEY.Query.getQueryDetails({
- containerPath: this.containerPath,
- schemaName: this.schemaName,
- queryName: this.queryName,
- viewName: viewName,
- fields: fields,
- initializeMissingView: true,
- success: function(queryDetails) {
- success.call(scope || this, queryDetails, viewName, userColumns, userFilter, userSort);
- },
- failure: failure,
- scope: scope
- });
- };
-
- /**
- * Hides the customize view interface if it is visible.
- */
- LABKEY.DataRegion.prototype.hideCustomizeView = function() {
- if (this.activePanelId === CUSTOM_VIEW_PANELID) {
- this.hideButtonPanel();
- }
- };
-
- /**
- * Show the customize view interface.
- * @param activeTab {[String]} Optional. One of "ColumnsTab", "FilterTab", or "SortTab". If no value is specified (or undefined), the ColumnsTab will be shown.
- */
- LABKEY.DataRegion.prototype.showCustomizeView = function(activeTab) {
- var region = this;
-
- var panelConfig = this.getPanelConfiguration(CUSTOM_VIEW_PANELID);
-
- if (!panelConfig) {
-
- // whistle while we wait
- var timerId = setTimeout(function() {
- timerId = 0;
- region.showLoadingMessage("Opening custom view designer...");
- }, 500);
-
- LABKEY.DataRegion.loadViewDesigner(function() {
-
- var success = function(queryDetails, viewName, userColumns, userFilter, userSort) {
- timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage();
-
- // If there was an error parsing the query, we won't be able to render the customize view panel.
- if (queryDetails.exception) {
- var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', this.containerPath, {
- schemaName: this.schemaName,
- 'query.queryName': this.queryName
- });
- var msg = LABKEY.Utils.encodeHtml(queryDetails.exception) +
- " View Source";
-
- this.showErrorMessage(msg);
- return;
- }
-
- this.customizeView = Ext4.create('LABKEY.internal.ViewDesigner.Designer', {
- renderTo: Ext4.getBody().createChild({tag: 'div', customizeView: true, style: {display: 'none'}}),
- activeTab: activeTab,
- dataRegion: this,
- containerPath : this.containerPath,
- schemaName: this.schemaName,
- queryName: this.queryName,
- viewName: viewName,
- query: queryDetails,
- userFilter: userFilter,
- userSort: userSort,
- userColumns: userColumns,
- userContainerFilter: this.getUserContainerFilter(),
- allowableContainerFilters: this.allowableContainerFilters
- });
-
- this.customizeView.on('viewsave', function(designer, savedViewsInfo, urlParameters) {
- _onViewSave.apply(this, [this, designer, savedViewsInfo, urlParameters]);
- }, this);
-
- this.customizeView.on({
- beforedeleteview: function(cv, revert) {
- _beforeViewDelete(region, revert);
- },
- deleteview: function(cv, success, json) {
- _onViewDelete(region, success, json);
- }
- });
-
- var first = true;
-
- // Called when customize view needs to be shown
- var showFn = function(id, panel, element, callback, scope) {
- if (first) {
- panel.hide();
- panel.getEl().appendTo(Ext4.get(element[0]));
- first = false;
- }
- panel.doLayout();
- $(panel.getEl().dom).slideDown(undefined, function() {
- panel.show();
- callback.call(scope);
- });
- };
-
- // Called when customize view needs to be hidden
- var hideFn = function(id, panel, element, callback, scope) {
- $(panel.getEl().dom).slideUp(undefined, function() {
- panel.hide();
- callback.call(scope);
- });
- };
-
- this.publishPanel(CUSTOM_VIEW_PANELID, this.customizeView, showFn, hideFn, this);
- this.showPanel(CUSTOM_VIEW_PANELID);
- };
- var failure = function() {
- timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage();
- };
-
- this.getQueryDetails(success, failure, this);
- }, region);
- }
- else {
- if (activeTab) {
- panelConfig.panel.setActiveDesignerTab(activeTab);
- }
- this.showPanel(CUSTOM_VIEW_PANELID);
- }
- };
-
- /**
- * @ignore
- * @private
- * Shows/Hides customize view depending on if it is currently shown
- */
- LABKEY.DataRegion.prototype.toggleShowCustomizeView = function() {
- if (this.activePanelId === CUSTOM_VIEW_PANELID) {
- this.hideCustomizeView();
- }
- else {
- this.showCustomizeView(undefined);
- }
- };
-
- var _defaultShow = function(panelId, panel, ribbon, cb, cbScope) {
- $('#' + panelId).slideDown(undefined, function() {
- cb.call(cbScope);
- });
- };
-
- var _defaultHide = function(panelId, panel, ribbon, cb, cbScope) {
- $('#' + panelId).slideUp(undefined, function() {
- cb.call(cbScope);
- });
- };
-
- // TODO this is a pretty bad prototype, consider using config parameter with backward compat option
- LABKEY.DataRegion.prototype.publishPanel = function(panelId, panel, showFn, hideFn, scope, friendlyName) {
- this.panelConfigurations[panelId] = {
- panelId: panelId,
- panel: panel,
- show: $.isFunction(showFn) ? showFn : _defaultShow,
- hide: $.isFunction(hideFn) ? hideFn : _defaultHide,
- scope: scope
- };
- if (friendlyName && friendlyName !== panelId)
- this.panelConfigurations[friendlyName] = this.panelConfigurations[panelId];
- return this;
- };
-
- LABKEY.DataRegion.prototype.getPanelConfiguration = function(panelId) {
- return this.panelConfigurations[panelId];
- };
-
- /**
- * @ignore
- * Hides any panel that is currently visible. Returns a callback once the panel is hidden.
- */
- LABKEY.DataRegion.prototype.hidePanel = function(callback, scope) {
- if (this.activePanelId) {
- var config = this.getPanelConfiguration(this.activePanelId);
- if (config) {
-
- // find the ribbon container
- var ribbon = _getDrawerSelector(this);
-
- config.hide.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() {
- this.activePanelId = undefined;
- ribbon.hide();
- if ($.isFunction(callback)) {
- callback.call(scope || this);
- }
- LABKEY.Utils.signalWebDriverTest("dataRegionPanelHide");
- $(this).trigger($.Event('afterpanelhide'), [this]);
- }, this);
- }
- }
- else {
- if ($.isFunction(callback)) {
- callback.call(scope || this);
- }
- }
- };
-
- LABKEY.DataRegion.prototype.showPanel = function(panelId, callback, scope) {
-
- var config = this.getPanelConfiguration(panelId);
-
- if (!config) {
- console.error('Unable to find panel for id (' + panelId + '). Use publishPanel() to register a panel to be shown.');
- return;
- }
-
- this.hideContext();
- this.hideMessage(true);
-
- this.hidePanel(function() {
- this.activePanelId = config.panelId;
-
- // ensure the ribbon is visible
- var ribbon = _getDrawerSelector(this);
- ribbon.show();
-
- config.show.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() {
- if ($.isFunction(callback)) {
- callback.call(scope || this);
- }
- LABKEY.Utils.signalWebDriverTest("dataRegionPanelShow");
- $(this).trigger($.Event('afterpanelshow'), [this]);
- }, this);
- }, this);
- };
-
- function _hasPanelOpen(dr) {
- return dr.activePanelId !== undefined;
- }
-
- function _hasButtonBarMenuOpen(dr) {
- return _getBarSelector(dr).find(".lk-menu-drop.open").length > 0;
- }
-
- /**
- * Returns true if the user has interacted with the DataRegion by changing
- * the selection, opening a button menu, or opening a panel.
- * @return {boolean}
- * @private
- */
- LABKEY.DataRegion.prototype.isUserInteracting = function () {
- return this.selectionModified || _hasPanelOpen(this) || _hasButtonBarMenuOpen(this);
- };
-
- //
- // Misc
- //
-
- /**
- * @private
- */
- var _initHeaderLocking = function() {
- if (this._allowHeaderLock === true) {
- this.hLock = new HeaderLock(this);
- }
- };
-
- /**
- * @private
- */
- var _initPanes = function() {
- var callbacks = _paneCache[this.name];
- if (callbacks) {
- var me = this;
- callbacks.forEach(function(config) {
- config.cb.call(config.scope || me, me);
- });
- delete _paneCache[this.name];
- }
- };
-
- /**
- * @private
- */
- var _initReport = function() {
- if (LABKEY.Utils.isObject(this.report)) {
- this.addMessage({
- html: [
- 'Name:',
- LABKEY.Utils.encodeHtml(this.report.name),
- 'Source:',
- LABKEY.Utils.encodeHtml(this.report.source)
- ].join(' '),
- part: 'report',
- });
- }
- };
-
- // These study specific functions/constants should be moved out of Data Region
- // and into their own dependency.
-
- var COHORT_LABEL = '/Cohort/Label';
- var ADV_COHORT_LABEL = '/InitialCohort/Label';
- var COHORT_ENROLLED = '/Cohort/Enrolled';
- var ADV_COHORT_ENROLLED = '/InitialCohort/Enrolled';
-
- /**
- * DO NOT CALL DIRECTLY. This method is private and only available for removing cohort/group filters
- * for this Data Region.
- * @param subjectColumn
- * @param groupNames
- * @private
- */
- LABKEY.DataRegion.prototype._removeCohortGroupFilters = function(subjectColumn, groupNames) {
- this.clearSelected({quiet: true});
- var params = _getParameters(this);
- var skips = [], i, p, k;
-
- var keys = [
- subjectColumn + COHORT_LABEL,
- subjectColumn + ADV_COHORT_LABEL,
- subjectColumn + COHORT_ENROLLED,
- subjectColumn + ADV_COHORT_ENROLLED
- ];
-
- if (LABKEY.Utils.isArray(groupNames)) {
- for (k=0; k < groupNames.length; k++) {
- keys.push(subjectColumn + '/' + groupNames[k]);
- }
- }
-
- for (i = 0; i < params.length; i++) {
- p = params[i][0];
- if (p.indexOf(this.name + '.') === 0) {
- for (k=0; k < keys.length; k++) {
- if (p.indexOf(keys[k] + '~') > -1) {
- skips.push(p);
- k = keys.length; // break loop
- }
- }
- }
- }
-
- _updateFilter(this, undefined, skips);
- };
-
- /**
- * DO NOT CALL DIRECTLY. This method is private and only available for replacing advanced cohort filters
- * for this Data Region. Remove if advanced cohorts are removed.
- * @param filter
- * @private
- */
- LABKEY.DataRegion.prototype._replaceAdvCohortFilter = function(filter) {
- this.clearSelected({quiet: true});
- var params = _getParameters(this);
- var skips = [], i, p;
-
- for (i = 0; i < params.length; i++) {
- p = params[i][0];
- if (p.indexOf(this.name + '.') === 0) {
- if (p.indexOf(COHORT_LABEL) > -1 || p.indexOf(ADV_COHORT_LABEL) > -1 || p.indexOf(COHORT_ENROLLED) > -1 || p.indexOf(ADV_COHORT_ENROLLED)) {
- skips.push(p);
- }
- }
- }
-
- _updateFilter(this, filter, skips);
- };
-
- /**
- * Looks for a column based on fieldKey, name, displayField, or caption (in that order)
- * @param columnIdentifier
- * @returns {*}
- */
- LABKEY.DataRegion.prototype.getColumn = function(columnIdentifier) {
-
- var column = null, // backwards compat
- isString = LABKEY.Utils.isString,
- cols = this.columns;
-
- if (isString(columnIdentifier) && LABKEY.Utils.isArray(cols)) {
- $.each(['fieldKey', 'name', 'displayField', 'caption'], function(i, key) {
- $.each(cols, function(c, col) {
- if (isString(col[key]) && col[key] == columnIdentifier) {
- column = col;
- return false;
- }
- });
- if (column) {
- return false;
- }
- });
- }
-
- return column;
- };
-
- /**
- * Returns a query config object suitable for passing into LABKEY.Query.selectRows() or other LABKEY.Query APIs.
- * @returns {Object} Object representing the query configuration that generated this grid.
- */
- LABKEY.DataRegion.prototype.getQueryConfig = function() {
- var config = {
- dataRegionName: this.name,
- dataRegionSelectionKey: this.selectionKey,
- schemaName: this.schemaName,
- viewName: this.viewName,
- sort: this.getParameter(this.name + SORT_PREFIX),
- // NOTE: The parameterized query values from QWP are included
- parameters: this.getParameters(false),
- containerFilter: this.containerFilter
- };
-
- if (this.queryName) {
- config.queryName = this.queryName;
- }
- else if (this.sql) {
- config.sql = this.sql;
- }
-
- var filters = this.getUserFilterArray();
- if (filters.length > 0) {
- config.filters = filters;
- }
-
- return config;
- };
-
- /**
- * Hide the ribbon panel. If visible the ribbon panel will be hidden.
- */
- LABKEY.DataRegion.prototype.hideButtonPanel = function() {
- this.hidePanel();
- this.showContext();
- this.showMessageArea();
- };
-
- /**
- * Allows for asynchronous rendering of the Data Region. This region must be in "async" mode for
- * this to do anything.
- * @function
- * @param {String} [renderTo] - The element ID where to render the data region. If not given it will default to
- * the current renderTo target is.
- */
- LABKEY.DataRegion.prototype.render = function(renderTo) {
- if (!this.RENDER_LOCK && this.async) {
- _convertRenderTo(this, renderTo);
- this.refresh();
- }
- };
-
- /**
- * Show a ribbon panel.
- *
- * first arg can be button on the button bar or target panel id/configuration
- */
-
- LABKEY.DataRegion.prototype.toggleButtonPanelHandler = function(panelButton) {
- _toggleButtonPanel( this, $(panelButton).attr('data-labkey-panel-toggle'), null, true);
- };
-
- LABKEY.DataRegion.prototype.showButtonPanel = function(panel, optionalTab) {
- _toggleButtonPanel(this, panel, optionalTab, false);
- };
-
- LABKEY.DataRegion.prototype.toggleButtonPanel = function(panel, optionalTab) {
- _toggleButtonPanel(this, panel, optionalTab, true);
- };
-
- var _toggleButtonPanel = function(dr, panel, optionalTab, toggle) {
- var ribbon = _getDrawerSelector(dr);
- // first check if this is a named panel instead of a button element
- var panelId, panelSel;
- if (typeof panel === 'string' && dr.getPanelConfiguration(panel))
- panelId = dr.getPanelConfiguration(panel).panelId;
- else
- panelId = panel;
-
- if (panelId) {
-
- panelSel = $('#' + panelId);
-
- // allow for toggling the state
- if (panelId === dr.activePanelId) {
- if (toggle) {
- dr.hideButtonPanel();
- return;
- }
- }
- else {
- // determine if the content needs to be moved to the ribbon
- if (ribbon.has(panelSel).length === 0) {
- panelSel.detach().appendTo(ribbon);
- }
-
- // determine if this panel has been registered
- if (!dr.getPanelConfiguration(panelId) && panelSel.length > 0) {
- dr.publishPanel(panelId, panelId);
- }
-
- dr.showPanel(panelId);
- }
- if (optionalTab)
- {
- var t = panelSel.find('a[data-toggle="tab"][href="#' + optionalTab + '"]');
- if (!t.length)
- t = panelSel.find('a[data-toggle="tab"][data-tabName="' + optionalTab + '"]');
- t.tab('show');
- }
- }
- };
-
- LABKEY.DataRegion.prototype.loadFaceting = function(cb, scope) {
-
- var region = this;
-
- var onLoad = function() {
- region.facetLoaded = true;
- if ($.isFunction(cb)) {
- cb.call(scope || this);
- }
- };
-
- LABKEY.requiresExt4ClientAPI(function() {
- if (LABKEY.devMode) {
- // should match study/ParticipantFilter.lib.xml
- LABKEY.requiresScript([
- '/study/ReportFilterPanel.js',
- '/study/ParticipantFilterPanel.js'
- ], function() {
- LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad);
- });
- }
- else {
- LABKEY.requiresScript('/study/ParticipantFilter.min.js', function() {
- LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad);
- });
- }
- }, this);
- };
-
- LABKEY.DataRegion.prototype.showFaceting = function() {
- if (this.facetLoaded) {
- if (!this.facet) {
- this.facet = LABKEY.dataregion.panel.Facet.display(this);
- }
- this.facet.toggleCollapse();
- }
- else {
- this.loadFaceting(this.showFaceting, this);
- }
- };
-
- LABKEY.DataRegion.prototype.on = function(evt, callback, scope) {
- // Prevent from handing back the jQuery event itself.
- $(this).bind(evt, function() { callback.apply(scope || this, $(arguments).slice(1)); });
- };
-
- LABKEY.DataRegion.prototype.one = function(evt, callback, scope) {
- $(this).one(evt, function() { callback.apply(scope || this, $(arguments).slice(1)); });
- };
-
- LABKEY.DataRegion.prototype._onButtonClick = function(buttonId) {
- var item = this.findButtonById(this.buttonBar.items, buttonId);
- if (item && $.isFunction(item.handler)) {
- try {
- return item.handler.call(item.scope || this, this);
- }
- catch(ignore) {}
- }
- return false;
- };
-
- LABKEY.DataRegion.prototype.findButtonById = function(items, id) {
- if (!items || !items.length || items.length <= 0) {
- return null;
- }
-
- var ret;
- for (var i = 0; i < items.length; i++) {
- if (items[i].id == id) {
- return items[i];
- }
- ret = this.findButtonById(items[i].items, id);
- if (null != ret) {
- return ret;
- }
- }
-
- return null;
- };
-
- LABKEY.DataRegion.prototype.headerLock = function() { return this._allowHeaderLock === true; };
-
- LABKEY.DataRegion.prototype.disableHeaderLock = function() {
- if (this.headerLock() && this.hLock) {
- this.hLock.disable();
- this.hLock = undefined;
- }
- };
-
- /**
- * Add or remove a summary statistic for a given column in the DataRegion query view.
- * @param viewName
- * @param colFieldKey
- * @param summaryStatName
- */
- LABKEY.DataRegion.prototype.toggleSummaryStatForCustomView = function(viewName, colFieldKey, summaryStatName) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- var colProviderNames = [];
- $.each(view.analyticsProviders, function(index, existingProvider) {
- if (existingProvider.fieldKey === colFieldKey)
- colProviderNames.push(existingProvider.name);
- });
-
- if (colProviderNames.indexOf(summaryStatName) === -1) {
- _addAnalyticsProviderToView.call(this, view, colFieldKey, summaryStatName, true);
- }
- else {
- _removeAnalyticsProviderFromView.call(this, view, colFieldKey, summaryStatName, true);
- }
- }
- }, null, this);
- };
-
- /**
- * Get the array of selected ColumnAnalyticsProviders for the given column FieldKey in a view.
- * @param viewName
- * @param colFieldKey
- * @param callback
- * @param callbackScope
- */
- LABKEY.DataRegion.prototype.getColumnAnalyticsProviders = function(viewName, colFieldKey, callback, callbackScope) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- var colProviderNames = [];
- $.each(view.analyticsProviders, function(index, existingProvider) {
- if (existingProvider.fieldKey === colFieldKey) {
- colProviderNames.push(existingProvider.name);
- }
- });
-
- if ($.isFunction(callback)) {
- callback.call(callbackScope, colProviderNames);
- }
- }
- }, null, this);
- };
-
- /**
- * Set the summary statistic ColumnAnalyticsProviders for the given column FieldKey in the view.
- * @param viewName
- * @param colFieldKey
- * @param summaryStatProviderNames
- */
- LABKEY.DataRegion.prototype.setColumnSummaryStatistics = function(viewName, colFieldKey, summaryStatProviderNames) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- var newAnalyticsProviders = [];
- $.each(view.analyticsProviders, function(index, existingProvider) {
- if (existingProvider.fieldKey !== colFieldKey || existingProvider.name.indexOf('AGG_') != 0) {
- newAnalyticsProviders.push(existingProvider);
- }
- });
-
- $.each(summaryStatProviderNames, function(index, providerName) {
- newAnalyticsProviders.push({
- fieldKey: colFieldKey,
- name: providerName,
- isSummaryStatistic: true
- });
- });
-
- view.analyticsProviders = newAnalyticsProviders;
- _updateSessionCustomView.call(this, view, true);
- }
- }, null, this);
- };
-
- /**
- * Used via SummaryStatisticsAnalyticsProvider to show a dialog of the applicable summary statistics for a column in the view.
- * @param colFieldKey
- */
- LABKEY.DataRegion.prototype.showColumnStatisticsDialog = function(colFieldKey) {
- LABKEY.requiresScript('query/ColumnSummaryStatistics', function() {
- var regionViewName = this.viewName || "",
- column = this.getColumn(colFieldKey);
-
- if (column) {
- this.getColumnAnalyticsProviders(regionViewName, colFieldKey, function(colSummaryStats) {
- Ext4.create('LABKEY.ext4.ColumnSummaryStatisticsDialog', {
- queryConfig: this.getQueryConfig(),
- filterArray: LABKEY.Filter.getFiltersFromUrl(this.selectAllURL, 'query'), //Issue 26594
- containerPath: this.containerPath,
- column: column,
- initSelection: colSummaryStats,
- listeners: {
- scope: this,
- applySelection: function(win, colSummaryStatsNames) {
- win.getEl().mask("Applying selection...");
- this.setColumnSummaryStatistics(regionViewName, colFieldKey, colSummaryStatsNames);
- win.close();
- }
- }
- }).show();
- }, this);
- }
- }, this);
- };
-
- /**
- * Remove a column from the given DataRegion query view.
- * @param viewName
- * @param colFieldKey
- */
- LABKEY.DataRegion.prototype.removeColumn = function(viewName, colFieldKey) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- var colFieldKeys = $.map(view.columns, function (c) {
- return c.fieldKey;
- }),
- fieldKeyIndex = colFieldKeys.indexOf(colFieldKey);
-
- if (fieldKeyIndex > -1) {
- view.columns.splice(fieldKeyIndex, 1);
- _updateSessionCustomView.call(this, view, true);
- }
- }
- }, null, this);
- };
-
- /**
- * Add the enabled analytics provider to the custom view definition based on the column fieldKey and provider name.
- * In addition, disable the column menu item if the column is visible in the grid.
- * @param viewName
- * @param colFieldKey
- * @param providerName
- */
- LABKEY.DataRegion.prototype.addAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- _addAnalyticsProviderToView.call(this, view, colFieldKey, providerName, false);
- _updateAnalyticsProviderMenuItem(this.name + ':' + colFieldKey, providerName, true);
- }
- }, null, this);
- };
-
- /**
- * Remove an enabled analytics provider from the custom view definition based on the column fieldKey and provider name.
- * In addition, enable the column menu item if the column is visible in the grid.
- * @param viewName
- * @param colFieldKey
- * @param providerName
- */
- LABKEY.DataRegion.prototype.removeAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
- this.getQueryDetails(function(queryDetails) {
- var view = _getViewFromQueryDetails(queryDetails, viewName);
- if (view && _viewContainsColumn(view, colFieldKey)) {
- _removeAnalyticsProviderFromView.call(this, view, colFieldKey, providerName, false);
- _updateAnalyticsProviderMenuItem(this.name + ':' + colFieldKey, providerName, false);
- }
- }, null, this);
- };
-
- /**
- * @private
- */
- LABKEY.DataRegion.prototype._openFilter = function(columnName, evt) {
- if (evt && $(evt.target).hasClass('fa-close')) {
- return;
- }
-
- var column = this.getColumn(columnName);
-
- if (column) {
- var show = function() {
- this._dialogLoaded = true;
- new LABKEY.FilterDialog({
- dataRegionName: this.name,
- column: this.getColumn(columnName),
- cacheFacetResults: false // could have changed on Ajax
- }).show();
- }.bind(this);
-
- this._dialogLoaded ? show() : LABKEY.requiresExt3ClientAPI(show);
- }
- else {
- LABKEY.Utils.alert('Column not available', 'Unable to find column "' + columnName + '" in this view.');
- }
- };
-
- var _updateSessionCustomView = function(customView, requiresRefresh) {
- var viewConfig = $.extend({}, customView, {
- shared: false,
- inherit: false,
- hidden: false,
- session: true
- });
-
- LABKEY.Query.saveQueryViews({
- containerPath: this.containerPath,
- schemaName: this.schemaName,
- queryName: this.queryName,
- views: [viewConfig],
- scope: this,
- success: function(info) {
- if (requiresRefresh) {
- this.refresh();
- }
- else if (info.views.length === 1) {
- this.view = info.views[0];
- _initCustomViews.call(this);
- this.showContext();
- }
- }
- });
- };
-
- var _addAnalyticsProviderToView = function(view, colFieldKey, providerName, isSummaryStatistic) {
- var colProviderNames = [];
- $.each(view.analyticsProviders, function(index, existingProvider) {
- if (existingProvider.fieldKey === colFieldKey)
- colProviderNames.push(existingProvider.name);
- });
-
- if (colProviderNames.indexOf(providerName) === -1) {
- view.analyticsProviders.push({
- fieldKey: colFieldKey,
- name: providerName,
- isSummaryStatistic: isSummaryStatistic
- });
-
- _updateSessionCustomView.call(this, view, isSummaryStatistic);
- }
- };
-
- var _removeAnalyticsProviderFromView = function(view, colFieldKey, providerName, isSummaryStatistic) {
- var indexToRemove = null;
- $.each(view.analyticsProviders, function(index, existingProvider) {
- if (existingProvider.fieldKey === colFieldKey && existingProvider.name === providerName) {
- indexToRemove = index;
- return false;
- }
- });
-
- if (indexToRemove != null) {
- view.analyticsProviders.splice(indexToRemove, 1);
- _updateSessionCustomView.call(this, view, isSummaryStatistic);
- }
- };
-
- /**
- * Attempt to find a DataRegion analytics provider column menu item so that it can be either enabled to allow
- * it to once again be selected after removal or disabled so that it can't be selected a second time.
- * @param columnName the DataRegion column th element column-name attribute
- * @param providerName the analytics provider name
- * @param disable
- * @private
- */
- var _updateAnalyticsProviderMenuItem = function(columnName, providerName, disable) {
- var menuItemEl = $("th[column-name|='" + columnName + "']").find("a[onclick*='" + providerName + "']").parent();
- if (menuItemEl) {
- if (disable) {
- menuItemEl.addClass('disabled');
- }
- else {
- menuItemEl.removeClass('disabled');
- }
- }
- };
-
- //
- // PRIVATE FUNCTIONS
- //
- var _applyOptionalParameters = function(region, params, optionalParams) {
- optionalParams.forEach(function(p) {
- if (LABKEY.Utils.isObject(p)) {
- if (region[p.name] !== undefined) {
- if (p.check && !p.check.call(region, region[p.name])) {
- return;
- }
- if (p.prefix) {
- params[region.name + '.' + p.name] = region[p.name];
- }
- else {
- params[p.name] = region[p.name];
- }
- }
- }
- else if (p && region[p] !== undefined) {
- params[p] = region[p];
- }
- });
- };
-
- var _alterSortString = function(region, current, fieldKey, direction /* optional */) {
- fieldKey = _resolveFieldKey(region, fieldKey);
-
- var columnName = fieldKey.toString(),
- newSorts = [];
-
- if (current != null) {
- current.split(',').forEach(function(sort) {
- if (sort.length > 0 && (sort != columnName) && (sort != SORT_ASC + columnName) && (sort != SORT_DESC + columnName)) {
- newSorts.push(sort);
- }
- });
- }
-
- if (direction === SORT_ASC) { // Easier to read without the encoded + on the URL...
- direction = '';
- }
-
- if (LABKEY.Utils.isString(direction)) {
- newSorts = [direction + columnName].concat(newSorts);
- }
-
- return newSorts.join(',');
- };
-
- var _ensureFilterDateFormat = function(value) {
- if (LABKEY.Utils.isDate(value)) {
- value = $.format.date(value, 'yyyy-MM-dd');
- if (LABKEY.Utils.endsWith(value, 'Z')) {
- value = value.substring(0, value.length - 1);
- }
- }
-
- return value;
- }
-
- var _buildQueryString = function(region, pairs) {
- if (!LABKEY.Utils.isArray(pairs)) {
- return '';
- }
-
- var queryParts = [], key, value;
-
- pairs.forEach(function(pair) {
- key = pair[0];
- value = pair.length > 1 ? pair[1] : undefined;
-
- queryParts.push(encodeURIComponent(key));
- if (LABKEY.Utils.isDefined(value)) {
-
- value = _ensureFilterDateFormat(value);
- queryParts.push('=');
- queryParts.push(encodeURIComponent(value));
- }
- queryParts.push('&');
- });
-
- if (queryParts.length > 0) {
- queryParts.pop();
- }
-
- return queryParts.join("");
- };
-
- var _chainSelectionCountCallback = function(region, config) {
-
- var success = LABKEY.Utils.getOnSuccess(config);
-
- // On success, update the current selectedCount on this DataRegion and fire the 'selectchange' event
- config.success = function(data) {
- region.selectionModified = true;
- region.selectedCount = data.count;
- _onSelectionChange(region);
-
- // Chain updateSelected with the user-provided success callback
- if ($.isFunction(success)) {
- success.call(config.scope, data);
- }
- };
-
- return config;
- };
-
- var _convertRenderTo = function(region, renderTo) {
- if (renderTo) {
- if (LABKEY.Utils.isString(renderTo)) {
- region.renderTo = renderTo;
- }
- else if (LABKEY.Utils.isString(renderTo.id)) {
- region.renderTo = renderTo.id; // support 'Ext' elements
- }
- else {
- throw 'Unsupported "renderTo"';
- }
- }
-
- return region;
- };
-
- var _deleteTimer;
-
- var _beforeViewDelete = function(region, revert) {
- _deleteTimer = setTimeout(function() {
- _deleteTimer = 0;
- region.showLoadingMessage(revert ? 'Reverting view...' : 'Deleting view...');
- }, 500);
- };
-
- var _onViewDelete = function(region, success, json) {
- if (_deleteTimer) {
- clearTimeout(_deleteTimer);
- }
-
- if (success) {
- region.removeMessage.call(region, 'customizeview');
- region.showSuccessMessage.call(region);
-
- // change view to either a shadowed view or the default view
- var config = { type: 'view' };
- if (json.viewName) {
- config.viewName = json.viewName;
- }
- region.changeView.call(region, config);
- }
- else {
- region.removeMessage.call(region, 'customizeview');
- region.showErrorMessage.call(region, json.exception);
- }
- };
-
- // The view can be reverted without ViewDesigner present
- var _revertCustomView = function(region) {
- _beforeViewDelete(region, true);
-
- var config = {
- schemaName: region.schemaName,
- queryName: region.queryName,
- containerPath: region.containerPath,
- revert: true,
- success: function(json) {
- _onViewDelete(region, true /* success */, json);
- },
- failure: function(json) {
- _onViewDelete(region, false /* success */, json);
- }
- };
-
- if (region.viewName) {
- config.viewName = region.viewName;
- }
-
- LABKEY.Query.deleteQueryView(config);
- };
-
- var _getViewFromQueryDetails = function(queryDetails, viewName) {
- var matchingView;
-
- $.each(queryDetails.views, function(index, view) {
- if (view.name === viewName) {
- matchingView = view;
- return false;
- }
- });
-
- return matchingView;
- };
-
- var _viewContainsColumn = function(view, colFieldKey) {
- var keys = $.map(view.columns, function(c) {
- return c.fieldKey.toLowerCase();
- });
- var exists = colFieldKey && keys.indexOf(colFieldKey.toLowerCase()) > -1;
-
- if (!exists) {
- console.warn('Unable to find column in view: ' + colFieldKey);
- }
-
- return exists;
- };
-
- var _getAllRowSelectors = function(region) {
- return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".toggle"]');
- };
-
- var _getBarSelector = function(region) {
- return $('#' + region.domId + '-headerbar');
- };
-
- var _getContextBarSelector = function(region) {
- return $('#' + region.domId + '-ctxbar');
- };
-
- var _getDrawerSelector = function(region) {
- return $('#' + region.domId + '-drawer');
- };
-
- var _getFormSelector = function(region) {
- var form = $('form#' + region.domId + '-form');
-
- // derived DataRegion's may not include the form id
- if (form.length === 0) {
- form = $('#' + region.domId).closest('form');
- }
-
- return form;
- };
-
- var _getHeaderSelector = function(region) {
- return $('#' + region.domId + '-header');
- };
-
- var _getRowSelectors = function(region) {
- return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".select"]');
- };
-
- var _getSectionSelector = function(region, dir) {
- return $('#' + region.domId + '-section-' + dir);
- };
-
- var _getShowFirstSelector = function(region) {
- return $('#' + region.showFirstID);
- };
-
- var _getShowLastSelector = function(region) {
- return $('#' + region.showLastID);
- };
-
- var _getShowAllSelector = function(region) {
- return $('#' + region.showAllID);
- };
-
- // Formerly, LABKEY.DataRegion.getParamValPairsFromString / LABKEY.DataRegion.getParamValPairs
- var _getParameters = function(region, skipPrefixSet /* optional */) {
-
- var params = [];
- var qString = region.requestURL;
-
- if (LABKEY.Utils.isString(qString) && qString.length > 0) {
-
- var qmIdx = qString.indexOf('?');
- if (qmIdx > -1) {
- qString = qString.substring(qmIdx + 1);
-
- var poundIdx = qString.indexOf('#');
- if (poundIdx > -1)
- qString = qString.substr(0, poundIdx);
-
- if (qString.length > 1) {
- var pairs = qString.split('&'), p, key,
- LAST = '.lastFilter', lastIdx, skip = LABKEY.Utils.isArray(skipPrefixSet);
-
- var exactMatches = EXACT_MATCH_PREFIXES.map(function (prefix) {
- return region.name + prefix;
- });
-
- $.each(pairs, function (i, pair) {
- p = pair.split('=', 2);
- key = p[0] = decodeURIComponent(p[0]);
- lastIdx = key.indexOf(LAST);
-
- if (lastIdx > -1 && lastIdx === (key.length - LAST.length)) {
- return;
- }
- else if (REQUIRE_NAME_PREFIX.hasOwnProperty(key)) {
- // Issue 26686: Block known parameters, should be prefixed by region name
- return;
- }
-
- var stop = false;
- if (skip) {
- $.each(skipPrefixSet, function (j, skipPrefix) {
- if (LABKEY.Utils.isString(skipPrefix)) {
-
- // Special prefix that should remove all filters, but no other parameters for the current grid
- if (skipPrefix.indexOf(ALL_FILTERS_SKIP_PREFIX) === (skipPrefix.length - 2)) {
- if (key.indexOf(region.name + '.') === 0 && key.indexOf('~') > 0) {
-
- stop = true;
- return false;
- }
- }
- else {
- if (exactMatches.indexOf(skipPrefix) > -1) {
- if (key === skipPrefix) {
- stop = true;
- return false;
- }
- }
- else if (key.toLowerCase().indexOf(skipPrefix.toLowerCase()) === 0) {
- // only skip filters, parameters, and sorts for the current grid
- if (key.indexOf(region.name + '.') === 0 &&
-
- (key === skipPrefix ||
- key.indexOf('~') > 0 ||
- key.indexOf(PARAM_PREFIX) > 0 ||
- key === (skipPrefix + 'sort'))) {
- stop = true;
- return false;
- }
- }
- }
- }
- });
- }
-
- if (!stop) {
- if (p.length > 1) {
- p[1] = decodeURIComponent(p[1]);
- }
- params.push(p);
- }
- });
- }
- }
- }
-
- return params;
- };
-
- /**
- *
- * @param region
- * @param {boolean} [asString=false]
- * @private
- */
- var _getUserSort = function(region, asString) {
- var userSort = [],
- sortParam = region.getParameter(region.name + SORT_PREFIX);
-
- if (asString) {
- userSort = sortParam || '';
- }
- else {
- if (sortParam) {
- var fieldKey, dir;
- sortParam.split(',').forEach(function(sort) {
- fieldKey = sort;
- dir = SORT_ASC;
- if (sort.charAt(0) === SORT_DESC) {
- fieldKey = fieldKey.substring(1);
- dir = SORT_DESC;
- }
- else if (sort.charAt(0) === SORT_ASC) {
- fieldKey = fieldKey.substring(1);
- }
- userSort.push({fieldKey: fieldKey, dir: dir});
- });
- }
- }
-
- return userSort;
- };
-
- var _getViewBarSelector = function(region) {
- return $('#' + region.domId + '-viewbar');
- };
-
- 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);
- }, region));
- };
-
- var _onRenderMessageArea = function(region, parts) {
- var msgArea = region.msgbox;
- if (msgArea) {
- if (region.showRecordSelectors && parts['selection']) {
- _buttonSelectionBind(region, '.select-all', region.selectAll);
- _buttonSelectionBind(region, '.select-none', region.clearSelected);
- _buttonSelectionBind(region, '.show-all', region.showAll);
- _buttonSelectionBind(region, '.show-selected', region.showSelectedRows);
- _buttonSelectionBind(region, '.show-unselected', region.showUnselectedRows);
- }
- else if (parts['customizeview']) {
- _buttonSelectionBind(region, '.unsavedview-revert', function() { _revertCustomView(this); });
- _buttonSelectionBind(region, '.unsavedview-edit', function() { this.showCustomizeView(undefined); });
- _buttonSelectionBind(region, '.unsavedview-save', function() { _saveSessionCustomView(this); });
- }
- }
- };
-
- var _onSelectionChange = function(region) {
- $(region).trigger('selectchange', [region, region.selectedCount]);
- _updateRequiresSelectionButtons(region, region.selectedCount);
- LABKEY.Utils.signalWebDriverTest('dataRegionUpdate', region.selectedCount);
- LABKEY.Utils.signalWebDriverTest('dataRegionUpdate-' + region.name, region.selectedCount);
- };
-
- var _onViewSave = function(region, designer, savedViewsInfo, urlParameters) {
- if (savedViewsInfo && savedViewsInfo.views.length > 0) {
- region.hideCustomizeView.call(region);
- region.changeView.call(region, {
- type: 'view',
- viewName: savedViewsInfo.views[0].name
- }, urlParameters);
- }
- };
-
- var _removeParameters = function(region, skipPrefixes /* optional */) {
- return _setParameters(region, null, skipPrefixes);
- };
-
- var _resolveFieldKey = function(region, fieldKey) {
- var fk = fieldKey;
- if (!(fk instanceof LABKEY.FieldKey)) {
- fk = LABKEY.FieldKey.fromString('' + fk);
- }
- return fk;
- };
-
- var _saveSessionCustomView = function(region) {
- // Note: currently only will save session views. Future version could create a new view using url sort/filters.
- if (!(region.view && region.view.session)) {
- return;
- }
-
- // Get the canEditSharedViews permission and candidate targetContainers.
- var viewName = (region.view && region.view.name) || region.viewName || '';
-
- LABKEY.Query.getQueryDetails({
- schemaName: region.schemaName,
- queryName: region.queryName,
- viewName: viewName,
- initializeMissingView: false,
- containerPath: region.containerPath,
- success: function (json) {
- // Display an error if there was an issue error getting the query details
- if (json.exception) {
- var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', null, {schemaName: this.schemaName, "query.queryName": this.queryName});
- var msg = LABKEY.Utils.encodeHtml(json.exception) + " View Source";
-
- this.showErrorMessage.call(this, msg);
- return;
- }
-
- _saveSessionShowPrompt(this, json);
- },
- scope: region
- });
- };
-
- var _saveSessionView = function(o, region, win) {
- var timerId = setTimeout(function() {
- timerId = 0;
- Ext4.Msg.progress("Saving...", "Saving custom view...");
- }, 500);
-
- var jsonData = {
- schemaName: region.schemaName,
- "query.queryName": region.queryName,
- "query.viewName": region.viewName,
- newName: o.name,
- inherit: o.inherit,
- shared: o.shared,
- hidden: o.hidden,
- replace: o.replace,
- };
-
- if (o.inherit) {
- jsonData.containerPath = o.containerPath;
- }
-
- LABKEY.Ajax.request({
- url: LABKEY.ActionURL.buildURL('query', 'saveSessionView', region.containerPath),
- method: 'POST',
- jsonData: jsonData,
- callback: function() {
- if (timerId > 0)
- clearTimeout(timerId);
- win.close();
- },
- success: function() {
- region.showSuccessMessage.call(region);
- region.changeView.call(region, {type: 'view', viewName: o.name});
- },
- failure: function(resp) {
- var json = resp.responseText ? Ext4.decode(resp.responseText) : resp;
- if (json.exception && json.exception.indexOf('A saved view by the name') === 0) {
-
- Ext4.Msg.show({
- title : "Duplicate View Name",
- msg : json.exception + " Would you like to replace it?",
- cls : 'data-window',
- icon : Ext4.Msg.QUESTION,
- buttons : Ext4.Msg.YESNO,
- fn : function(btn) {
- if (btn === 'yes') {
- o.replace = true;
- _saveSessionView(o, region, win);
- }
- },
- scope : this
- });
- }
- else
- Ext4.Msg.alert('Error saving view', json.exception || json.statusText || Ext4.decode(json.responseText).exception);
- },
- scope: region
- });
- };
-
- var _saveSessionShowPrompt = function(region, queryDetails) {
- LABKEY.DataRegion.loadViewDesigner(function() {
- var config = Ext4.applyIf({
- allowableContainerFilters: region.allowableContainerFilters,
- targetContainers: queryDetails.targetContainers,
- canEditSharedViews: queryDetails.canEditSharedViews,
- canEdit: LABKEY.DataRegion.getCustomViewEditableErrors(config).length == 0,
- success: function (win, o) {
- _saveSessionView(o, region, win);
- },
- scope: region
- }, region.view);
-
- LABKEY.internal.ViewDesigner.Designer.saveCustomizeViewPrompt(config);
- });
- };
-
- var _setParameter = function(region, param, value, skipPrefixes /* optional */) {
- _setParameters(region, [[param, value]], skipPrefixes);
- };
-
- var _setParameters = function(region, newParamValPairs, skipPrefixes /* optional */) {
- // prepend region name
- // e.g. ['.hello', '.goodbye'] becomes ['aqwp19.hello', 'aqwp19.goodbye']
- if (LABKEY.Utils.isArray(skipPrefixes)) {
- skipPrefixes.forEach(function(skip, i) {
- if (skip && skip.indexOf(region.name + '.') !== 0) {
- skipPrefixes[i] = region.name + skip;
- }
- });
- }
-
- var param, value,
- params = _getParameters(region, skipPrefixes);
-
- if (LABKEY.Utils.isArray(newParamValPairs)) {
- newParamValPairs.forEach(function(newPair) {
- if (!LABKEY.Utils.isArray(newPair)) {
- throw new Error("DataRegion: _setParameters newParamValPairs improperly initialized. It is an array of arrays. You most likely passed in an array of strings.");
- }
- param = newPair[0];
- value = newPair[1];
-
- // Allow value to be null/undefined to support no-value filter types (Is Blank, etc)
- if (LABKEY.Utils.isString(param) && param.length > 1) {
- if (param.indexOf(region.name) !== 0) {
- param = region.name + param;
- }
-
- params.push([param, value]);
- }
- });
- }
-
- if (region.async) {
- _load(region, params, skipPrefixes);
- }
- else {
- region.setSearchString.call(region, region.name, _buildQueryString(region, params));
- }
- };
-
- var _showRows = function(region, showRowsEnum) {
- // no need to re-query for totalRowCount, if async
- this.skipTotalRowCount = true;
-
- // clear sibling parameters, could we do this with events?
- this.maxRows = undefined;
- this.offset = 0;
-
- _setParameter(region, SHOW_ROWS_PREFIX, showRowsEnum, [OFFSET_PREFIX, MAX_ROWS_PREFIX, SHOW_ROWS_PREFIX]);
- };
-
- var _showSelectMessage = function(region, msg) {
- if (region.showRecordSelectors) {
- if (region.totalRows && region.totalRows != region.selectedCount) {
- msg += " Select All Rows";
- }
-
- msg += " " + "Select None";
- var showOpts = [];
- if (region.showRows !== 'all' && !_isMaxRowsAllRows(region))
- showOpts.push("Show All");
- if (region.showRows !== 'selected')
- showOpts.push("Show Selected");
- if (region.showRows !== 'unselected')
- showOpts.push("Show Unselected");
- msg += " " + showOpts.join(" ");
- }
-
- // add the record selector message, the link handlers will get added after render in _onRenderMessageArea
- region.addMessage.call(region, msg, 'selection');
- };
-
- var _toggleAllRows = function(region, checked) {
- var ids = [];
-
- _getRowSelectors(region).each(function() {
- if (!this.disabled) {
- this.checked = checked;
- ids.push(this.value);
- }
- });
-
- _getAllRowSelectors(region).each(function() { this.checked = checked === true; });
- return ids;
- };
-
- /**
- * Asynchronous loader for a DataRegion
- * @param region {DataRegion}
- * @param [newParams] {string}
- * @param [skipPrefixes] {string[]}
- * @param [callback] {Function}
- * @param [scope]
- * @private
- */
- var _load = function(region, newParams, skipPrefixes, callback, scope) {
-
- var params = _getAsyncParams(region, newParams ? newParams : _getParameters(region), skipPrefixes);
- var jsonData = _getAsyncBody(region, params);
-
- // TODO: This should be done in _getAsyncParams, but is not since _getAsyncBody relies on it. Refactor it.
- // ensure SQL is not on the URL -- we allow any property to be pulled through when creating parameters.
- if (params.sql) {
- delete params.sql;
- }
-
- /**
- * The target jQuery element that will be either written to or replaced
- */
- var target;
-
- /**
- * Flag used to determine if we should replace target element (default) or write to the target contents
- * (used during QWP render for example)
- * @type {boolean}
- */
- var useReplace = true;
-
- /**
- * The string identifier for where the region will render. Mainly used to display useful messaging upon failure.
- * @type {string}
- */
- var renderEl;
-
- if (region.renderTo) {
- useReplace = false;
- renderEl = region.renderTo;
- target = $('#' + region.renderTo);
- }
- else if (!region.domId) {
- throw '"renderTo" must be specified either upon construction or when calling render()';
- }
- else {
- renderEl = region.domId;
- target = $('#' + region.domId);
-
- // attempt to find the correct node to render to...
- var form = _getFormSelector(region);
- if (form.length && form.parent('div').length) {
- target = form.parent('div');
- }
- else {
- // next best render target
- throw 'unable to find a good target element. Perhaps this region is not using the standard renderer?'
- }
- }
- var timerId = setTimeout(function() {
- timerId = 0;
- if (target) {
- target.html("
" +
- "
loading...
" +
- "
");
- }
- }, 500);
-
- LABKEY.Ajax.request({
- timeout: region.timeout === undefined ? DEFAULT_TIMEOUT : region.timeout,
- url: LABKEY.ActionURL.buildURL('project', 'getWebPart.api', region.containerPath),
- method: 'POST',
- params: params,
- jsonData: jsonData,
- success: function(response) {
- if (timerId > 0) {
- clearTimeout(timerId);//load mask task no longer needed
- }
- this.hidePanel(function() {
- if (target.length) {
-
- this.destroy();
-
- LABKEY.Utils.loadAjaxContent(response, target, function() {
-
- if ($.isFunction(callback)) {
- callback.call(scope);
- }
-
- if ($.isFunction(this._success)) {
- this._success.call(this.scope || this, this, response);
- }
-
- $(this).trigger('success', [this, response]);
-
- this.RENDER_LOCK = true;
- $(this).trigger('render', this);
- this.RENDER_LOCK = false;
- }, this, useReplace);
- }
- else {
- // not finding element considered a failure
- if ($.isFunction(this._failure)) {
- this._failure.call(this.scope || this, response /* json */, response, undefined /* options */, target);
- }
- else if (!this.suppressRenderErrors) {
- LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
- }
- }
- }, this);
- },
- failure: LABKEY.Utils.getCallbackWrapper(function(json, response, options) {
-
- if (target.length) {
- if ($.isFunction(this._failure)) {
- this._failure.call(this.scope || this, json, response, options);
- }
- else if (this.errorType === 'html') {
- if (useReplace) {
- target.replaceWith('
' + LABKEY.Utils.encodeHtml(json.exception) + '
');
- }
- else {
- target.html('
' + LABKEY.Utils.encodeHtml(json.exception) + '
');
- }
- }
- }
- else if (!this.suppressRenderErrors) {
- LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
- }
- }, region, true),
- scope: region
- });
-
- if (region.async && !region.complete && region.showPaginationCountAsync && !region.skipTotalRowCount) {
- _loadAsyncTotalRowCount(region, params, jsonData);
- }
- region.skipTotalRowCount = false;
- };
-
- var totalRowCountRequests = {}; // track the request per region name so that we cancel the correct request when necessary
- var _loadAsyncTotalRowCount = function(region, params, jsonData) {
- // if there is a previous request pending, abort it before starting a new one
- var totalRowCountRequest = totalRowCountRequests[region.name];
- if (totalRowCountRequest !== undefined) {
- totalRowCountRequest.abort();
- }
-
- region.totalRows = undefined;
- region.loadingTotalRows = true;
-
- totalRowCountRequests[region.name] = LABKEY.Query.selectRows({
- ...region.getQueryConfig(),
- method: 'POST',
- containerPath: region.containerPath,
- filterArray: LABKEY.Filter.getFiltersFromParameters({ ...params, ...jsonData.filters }, params.dataRegionName),
- sort: undefined,
- maxRows: 1,
- offset: 0,
- includeMetadata: false,
- includeDetailsColumn: false,
- includeUpdateColumn: false,
- includeTotalCount: true,
- success: function(json) {
- totalRowCountRequests[region.name] = undefined;
- region.loadingTotalRows = false;
-
- if (json !== undefined && json.rowCount !== undefined) {
- region.totalRows = json.rowCount;
-
- // update the pagination button disabled state for 'Show Last' and 'Show All' since they include the totalRows count in their calc
- var showLast = _showLastEnabled(region);
- if (showLast) {
- _getShowLastSelector(region).parent('li').removeClass('disabled');
- _getShowAllSelector(region).parent('li').removeClass('disabled');
- }
- }
- // note: use _getFormSelector instead of _getBarSelector so that we get the floating header as well
- _getFormSelector(region).find('a.labkey-paginationText').html(_getPaginationText(region));
- },
- failure: function(error, request) {
- var aborted = request.status === 0;
- if (!aborted) {
- console.error(error);
- totalRowCountRequests[region.name] = undefined;
- region.loadingTotalRows = false;
- _getFormSelector(region).find('a.labkey-paginationText').html(_getPaginationText(region));
- }
- }
- });
- };
-
- var _getAsyncBody = function(region, params) {
- var json = {};
-
- if (params.sql) {
- json.sql = LABKEY.Utils.wafEncode(params.sql);
- }
-
- _processButtonBar(region, json);
-
- // Issue 10505: add non-removable sorts and filters to json (not url params).
- if (region.sort || region.filters || region.aggregates) {
- json.filters = {};
-
- if (region.filters) {
- LABKEY.Filter.appendFilterParams(json.filters, region.filters, region.name);
- }
-
- if (region.sort) {
- json.filters[region.dataRegionName + SORT_PREFIX] = region.sort;
- }
-
- if (region.aggregates) {
- LABKEY.Filter.appendAggregateParams(json.filters, region.aggregates, region.name);
- }
- }
-
- if (region.metadata) {
- json.metadata = {
- type: region.metadata.type,
- value: LABKEY.Utils.wafEncode(region.metadata.value)
- };
- }
-
- return json;
- };
-
- var _processButtonBar = function(region, json) {
-
- var bar = region.buttonBar;
-
- if (bar && (bar.position || (bar.items && bar.items.length > 0))) {
- _processButtonBarItems(region, bar.items);
-
- // only attach if valid
- json.buttonBar = bar;
- }
- };
-
- var _processButtonBarItems = function(region, items) {
- if (LABKEY.Utils.isArray(items) && items.length > 0) {
- for (var i = 0; i < items.length; i++) {
- var item = items[i];
-
- if (item && $.isFunction(item.handler)) {
- item.id = item.id || LABKEY.Utils.id();
- // TODO: A better way? This exposed _onButtonClick isn't very awesome
- item.onClick = "return LABKEY.DataRegions['" + region.name + "']._onButtonClick('" + item.id + "');";
- }
-
- if (item.items) {
- _processButtonBarItems(region, item.items);
- }
- }
- }
- };
-
- var _isFilter = function(region, parameter) {
- return parameter && parameter.indexOf(region.name + '.') === 0 && parameter.indexOf('~') > 0;
- };
-
- var _getAsyncParams = function(region, newParams, skipPrefixes) {
-
- var params = {};
- var name = region.name;
-
- //
- // Certain parameters are only included if the region is 'async'. These
- // were formerly a part of Query Web Part.
- //
- if (region.async) {
- params[name + '.async'] = true;
-
- if (LABKEY.Utils.isString(region.frame)) {
- params['webpart.frame'] = region.frame;
- }
-
- if (LABKEY.Utils.isString(region.bodyClass)) {
- params['webpart.bodyClass'] = region.bodyClass;
- }
-
- if (LABKEY.Utils.isString(region.title)) {
- params['webpart.title'] = region.title;
- }
-
- if (LABKEY.Utils.isString(region.titleHref)) {
- params['webpart.titleHref'] = region.titleHref;
- }
-
- if (LABKEY.Utils.isString(region.columns)) {
- params[region.name + '.columns'] = region.columns;
- }
-
- _applyOptionalParameters(region, params, [
- 'allowChooseQuery',
- 'allowChooseView',
- 'allowHeaderLock',
- 'buttonBarPosition',
- 'detailsURL',
- 'deleteURL',
- 'importURL',
- 'insertURL',
- 'linkTarget',
- 'updateURL',
- 'shadeAlternatingRows',
- 'showBorders',
- 'showDeleteButton',
- 'showDetailsColumn',
- 'showExportButtons',
- 'showRStudioButton',
- 'showImportDataButton',
- 'showInsertNewButton',
- 'showPagination',
- 'showPaginationCount',
- 'showReports',
- 'showSurroundingBorder',
- 'showFilterDescription',
- 'showUpdateColumn',
- 'showViewPanel',
- 'timeout',
- {name: 'disableAnalytics', prefix: true},
- {name: 'maxRows', prefix: true, check: function(v) { return v > 0; }},
- {name: 'showRows', prefix: true},
- {name: 'offset', prefix: true, check: function(v) { return v !== 0; }},
- {name: 'reportId', prefix: true},
- {name: 'viewName', prefix: true}
- ]);
-
- // Sorts configured by the user when interacting with the grid. We need to pass these as URL parameters.
- if (LABKEY.Utils.isString(region._userSort) && region._userSort.length > 0) {
- params[name + SORT_PREFIX] = region._userSort;
- }
-
- if (region.userFilters) {
- $.each(region.userFilters, function(filterExp, filterValue) {
- if (params[filterExp] == undefined) {
- params[filterExp] = [];
- }
- params[filterExp].push(filterValue);
- });
- region.userFilters = {}; // they've been applied
- }
-
- // TODO: Get rid of this and incorporate it with the normal containerFilter checks
- if (region.userContainerFilter) {
- params[name + CONTAINER_FILTER_NAME] = region.userContainerFilter;
- }
-
- if (region.parameters) {
- var paramPrefix = name + PARAM_PREFIX;
- $.each(region.parameters, function(parameter, value) {
- var key = parameter;
- if (parameter.indexOf(paramPrefix) !== 0) {
- key = paramPrefix + parameter;
- }
- params[key] = value;
- });
- }
- }
-
- //
- // apply all parameters
- //
-
- var newParamPrefixes = {};
-
- if (newParams) {
- newParams.forEach(function(pair) {
- // Issue 25337: Filters may repeat themselves
- if (_isFilter(region, pair[0])) {
- if (params[pair[0]] == undefined) {
- params[pair[0]] = [];
- }
- else if (!LABKEY.Utils.isArray(params[pair[0]])) {
- params[pair[0]] = [params[pair[0]]];
- }
-
- var value = pair[1];
-
- // Issue 47735: QWP date filter not being formatted
- // This needs to be formatted for the response passed back to the grid for the filter display and
- // filter dialog to render correctly
- value = _ensureFilterDateFormat(value);
-
- params[pair[0]].push(value);
- }
- else {
- params[pair[0]] = pair[1];
- }
-
- newParamPrefixes[pair[0]] = true;
- });
- }
-
- // Issue 40226: Don't include parameters that are being logically excluded
- if (skipPrefixes) {
- skipPrefixes.forEach(function(skipKey) {
- if (params.hasOwnProperty(skipKey) && !newParamPrefixes.hasOwnProperty(skipKey)) {
- delete params[skipKey];
- }
- });
- }
-
- //
- // Properties that cannot be modified
- //
-
- params.dataRegionName = region.name;
- params.schemaName = region.schemaName;
- params.viewName = region.viewName;
- params.reportId = region.reportId;
- params.returnUrl = window.location.href;
- params['webpart.name'] = 'Query';
-
- if (region.queryName) {
- params.queryName = region.queryName;
- }
- else if (region.sql) {
- params.sql = region.sql;
- }
-
- var key = region.name + CONTAINER_FILTER_NAME;
- var cf = region.getContainerFilter.call(region);
- if (cf && !(key in params)) {
- params[key] = cf;
- }
-
- return params;
- };
-
- var _updateFilter = function(region, filter, skipPrefixes) {
- var params = [];
- if (filter) {
- params.push([filter.getURLParameterName(region.name), filter.getURLParameterValue()]);
- }
- _setParameters(region, params, [OFFSET_PREFIX].concat(skipPrefixes));
- };
-
- var _updateRequiresSelectionButtons = function(region, selectedCount) {
-
- // update the 'select all on page' checkbox state
- _getAllRowSelectors(region).each(function() {
- if (region.isPageSelected.call(region)) {
- this.checked = true;
- this.indeterminate = false;
- }
- else if (region.selectedCount > 0) {
- // There are rows selected, but the are not visible on this page.
- this.checked = false;
- this.indeterminate = true;
- }
- else {
- this.checked = false;
- this.indeterminate = false;
- }
- });
-
- // 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 + ' rows selected.' :
- 'Selected ' + region.selectedCount + ' of ' + region.totalRows + ' rows.';
- _showSelectMessage(region, msg);
- }
-
- // Issue 10566: for javascript perf on IE stash the requires selection buttons
- if (!region._requiresSelectionButtons) {
- // escape ', ", and \
- var escaped = region.name.replace(/('|"|\\)/g, "\\$1");
- region._requiresSelectionButtons = $("a[data-labkey-requires-selection='" + escaped + "']");
- }
-
- region._requiresSelectionButtons.each(function() {
- var el = $(this);
-
- var isDropdown = false;
- var dropdownBtn = el.parent();
- if (dropdownBtn && dropdownBtn.hasClass('lk-menu-drop') && dropdownBtn.hasClass('dropdown'))
- isDropdown = true;
-
- // handle min-count
- var minCount = el.attr('data-labkey-requires-selection-min-count');
- if (minCount) {
- minCount = parseInt(minCount);
- }
- if (minCount === undefined) {
- minCount = 1;
- }
-
- // handle max-count
- var maxCount = el.attr('data-labkey-requires-selection-max-count');
- if (maxCount) {
- maxCount = parseInt(maxCount);
- }
-
- if (minCount <= selectedCount && (!maxCount || maxCount >= selectedCount)) {
- el.removeClass('labkey-disabled-button');
- if (isDropdown)
- dropdownBtn.removeClass('labkey-disabled-button');
- }
- else {
- el.addClass('labkey-disabled-button');
- if (isDropdown)
- dropdownBtn.addClass('labkey-disabled-button');
- }
- });
- };
-
- var HeaderLock = function(region) {
-
- // init
- if (!region.headerLock()) {
- region._allowHeaderLock = false;
- return;
- }
-
- this.region = region;
-
- var table = $('#' + region.domId);
- var firstRow = table.find('tr.labkey-alternate-row').first().children('td');
-
- // If no data rows exist just turn off header locking
- if (firstRow.length === 0) {
- firstRow = table.find('tr.labkey-row').first().children('td');
- if (firstRow.length === 0) {
- region._allowHeaderLock = false;
- return;
- }
- }
-
- var headerRowId = region.domId + '-column-header-row';
- var headerRow = $('#' + headerRowId);
-
- if (headerRow.length === 0) {
- region._allowHeaderLock = false;
- return;
- }
-
- var BOTTOM_OFFSET = 100;
-
- var me = this,
- timeout,
- locked = false,
- lastLeft = 0,
- pos = [ 0, 0, 0, 0 ],
- domObserver = null;
-
- // init
- var floatRow = headerRow
- .clone()
- // TODO: Possibly namespace all the ids underneath
- .attr('id', headerRowId + '-float')
- .css({
- 'box-shadow': '0 4px 4px #DCDCDC',
- display: 'none',
- position: 'fixed',
- top: 0,
- 'z-index': 2
- });
-
- floatRow.insertAfter(headerRow);
-
- // respect showPagination but do not use it directly as it may change
- var isPagingFloat = region.showPagination;
- var floatPaging, floatPagingWidth = 0;
-
- if (isPagingFloat) {
- var pageWidget = _getBarSelector(region).find('.labkey-pagination');
- if (pageWidget.children().length) {
- floatPaging = $('')
- .css({
- 'background-color': 'white',
- 'box-shadow': '0 4px 4px #DCDCDC',
- display: 'none',
- 'min-width': pageWidget.width(),
- opacity: 0.7,
- position: 'fixed',
- top: floatRow.height(),
- 'z-index': 1
- })
- .on('mouseover', function() {
- $(this).css('opacity', '1.0');
- })
- .on('mouseout', function() {
- $(this).css('opacity', '0.7')
- });
-
- var floatingPageWidget = pageWidget.clone(true).css('padding', '4px 8px');
-
- // adjust padding when buttons aren't shown
- if (!pageWidget.find('.btn-group').length) {
- floatingPageWidget.css('padding-bottom', '8px')
- }
-
- floatPaging.append(floatingPageWidget);
- table.parent().append(floatPaging);
- floatPagingWidth = floatPaging.width();
- } else {
- isPagingFloat = false;
- }
- }
-
- var disable = function() {
- me.region._allowHeaderLock = false;
-
- if (timeout) {
- clearTimeout(timeout);
- }
-
- $(window)
- .unbind('load', domTask)
- .unbind('resize', resizeTask)
- .unbind('scroll', onScroll);
-
- if (domObserver) {
- domObserver.disconnect();
- domObserver = null;
- }
- };
-
- /**
- * Configures the 'pos' array containing the following values:
- * [0] - X-coordinate of the top of the object relative to the offset parent.
- * [1] - Y-coordinate of the top of the object relative to the offset parent.
- * [2] - Y-coordinate of the bottom of the object.
- * [3] - width of the object
- * This method assumes interaction with the Header of the Data Region.
- */
- var loadPosition = function() {
- var header = headerRow.offset() || {top: 0};
- var table = $('#' + region.domId);
-
- var bottom = header.top + table.height() - BOTTOM_OFFSET;
- var width = headerRow.width();
- pos = [ header.left, header.top, bottom, width ];
- };
-
- loadPosition();
-
- var onResize = function() {
- loadPosition();
- var sub_h = headerRow.find('th');
-
- floatRow.width(headerRow.width()).find('th').each(function(i, el) {
- $(el).width($(sub_h[i]).width());
- });
-
- isPagingFloat && floatPaging.css({
- left: pos[0] - window.pageXOffset + floatRow.width() - floatPaging.width(),
- top: floatRow.height()
- });
- };
-
- /**
- * WARNING: This function is called often. Performance implications for each line.
- */
- var onScroll = function() {
- if (window.pageYOffset >= pos[1] && window.pageYOffset < pos[2]) {
- var newLeft = pos[0] - window.pageXOffset;
- var newPagingLeft = isPagingFloat ? newLeft + pos[3] - floatPagingWidth : 0;
-
- var floatRowCSS = {
- top: 0
- };
- var pagingCSS = isPagingFloat && {
- top: floatRow.height()
- };
-
- if (!locked) {
- locked = true;
- floatRowCSS.display = 'table-row';
- floatRowCSS.left = newLeft;
-
- pagingCSS.display = 'block';
- pagingCSS.left = newPagingLeft;
- }
- else if (lastLeft !== newLeft) {
- floatRowCSS.left = newLeft;
-
- pagingCSS.left = newPagingLeft;
- }
-
- floatRow.css(floatRowCSS);
- isPagingFloat && floatPaging.css(pagingCSS);
-
- lastLeft = newLeft;
- }
- else if (locked && window.pageYOffset >= pos[2]) {
- var newTop = pos[2] - window.pageYOffset;
-
- floatRow.css({
- top: newTop
- });
-
- isPagingFloat && floatPaging.css({
- top: newTop + floatRow.height()
- });
- }
- else if (locked) {
- locked = false;
- floatRow.hide();
- isPagingFloat && floatPaging.hide();
- }
- };
-
- var resizeTask = function(immediate) {
- clearTimeout(timeout);
- if (immediate) {
- onResize();
- }
- else {
- timeout = setTimeout(onResize, 110);
- }
- };
-
- var isDOMInit = false;
-
- var domTask = function() {
- if (!isDOMInit) {
- isDOMInit = true;
- // fire immediate to prevent flicker of components when reloading region
- resizeTask(true);
- }
- else {
- resizeTask();
- }
- onScroll();
- };
-
- $(window)
- .one('load', domTask)
- .on('resize', resizeTask)
- .on('scroll', onScroll);
-
- domObserver = new MutationObserver(mutationList =>
- mutationList.filter(m => m.type === 'childList').forEach(m => {
- m.addedNodes.forEach(domTask);
- }));
- domObserver.observe(document,{childList: true, subtree: true}); // Issue 13121, 50939
-
- // ensure that resize/scroll fire at the end of initialization
- domTask();
-
- return {
- disable: disable
- }
- };
-
- //
- // LOADER
- //
- LABKEY.DataRegion.create = function(config) {
-
- var region = LABKEY.DataRegions[config.name];
-
- if (region) {
- // region already exists, update properties
- $.each(config, function(key, value) {
- region[key] = value;
- });
- if (!config.view) {
- // when switching back to 'default' view, needs to clear region.view
- region.view = undefined;
- }
- _init.call(region, config);
- }
- else {
- // instantiate a new region
- region = new LABKEY.DataRegion(config);
- LABKEY.DataRegions[region.name] = region;
- }
-
- return region;
- };
-
- LABKEY.DataRegion.loadViewDesigner = function(cb, scope) {
- LABKEY.requiresExt4Sandbox(function() {
- LABKEY.requiresScript('internal/ViewDesigner', cb, scope);
- });
- };
-
- LABKEY.DataRegion.getCustomViewEditableErrors = function(customView) {
- var errors = [];
- if (customView && !customView.editable) {
- errors.push("The view is read-only and cannot be edited.");
- }
- return errors;
- };
-
- LABKEY.DataRegion.registerPane = function(regionName, callback, scope) {
- var region = LABKEY.DataRegions[regionName];
- if (region) {
- callback.call(scope || region, region);
- return;
- }
- else if (!_paneCache[regionName]) {
- _paneCache[regionName] = [];
- }
-
- _paneCache[regionName].push({cb: callback, scope: scope});
- };
-
- LABKEY.DataRegion.selectAll = function(config) {
- var params = {};
- if (!config.url) {
- // DataRegion doesn't have selectAllURL so generate url and query parameters manually
- config.url = LABKEY.ActionURL.buildURL('query', 'selectAll.api', config.containerPath);
-
- config.dataRegionName = config.dataRegionName || 'query';
-
- params = LABKEY.Query.buildQueryParams(
- config.schemaName,
- config.queryName,
- config.filters,
- null,
- config.dataRegionName
- );
-
- if (config.viewName)
- params[config.dataRegionName + VIEWNAME_PREFIX] = config.viewName;
-
- if (config.containerFilter)
- params.containerFilter = config.containerFilter;
-
- if (config.selectionKey)
- params[config.dataRegionName + '.selectionKey'] = config.selectionKey;
-
- $.each(config.parameters, function(propName, value) {
- params[config.dataRegionName + PARAM_PREFIX + propName] = value;
- });
-
- if (config.ignoreFilter) {
- params[config.dataRegionName + '.ignoreFilter'] = true;
- }
-
- // NOTE: ignore maxRows, showRows, and offset
- }
-
- LABKEY.Ajax.request({
- 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)
- });
- };
-
- /**
- * Static method to add or remove items from the selection for a given {@link #selectionKey}.
- *
- * @param config A configuration object with the following properties:
- * @param {String} config.selectionKey See {@link #selectionKey}.
- * @param {Array} config.ids Array of primary key ids for each row to select/unselect.
- * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected.
- * @param {Function} config.success The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'count' to indicate the updated selection count.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- *
- * @see LABKEY.DataRegion#getSelected
- * @see LABKEY.DataRegion#clearSelected
- */
- LABKEY.DataRegion.setSelected = function(config) {
- LABKEY.Ajax.request({
- url: LABKEY.ActionURL.buildURL('query', 'setSelected.api', config.containerPath),
- method: 'POST',
- jsonData: {
- checked: config.checked,
- id: config.ids || config.id,
- key: config.selectionKey,
- },
- scope: config.scope,
- success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
- failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
- });
- };
-
- /**
- * Static method to clear all selected items for a given {@link #selectionKey}.
- *
- * @param config A configuration object with the following properties:
- * @param {String} config.selectionKey See {@link #selectionKey}.
- * @param {Function} config.success The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'count' of 0 to indicate an empty selection.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- *
- * @see LABKEY.DataRegion#setSelected
- * @see LABKEY.DataRegion#getSelected
- */
- LABKEY.DataRegion.clearSelected = function(config) {
- LABKEY.Ajax.request({
- url: LABKEY.ActionURL.buildURL('query', 'clearSelected.api', config.containerPath),
- method: 'POST',
- jsonData: { key: config.selectionKey },
- success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
- failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
- });
- };
-
- /**
- * Static method to get all selected items for a given {@link #selectionKey}.
- *
- * @param config A configuration object with the following properties:
- * @param {String} config.selectionKey See {@link #selectionKey}.
- * @param {Function} config.success The function to be called upon success of the request.
- * The callback will be passed the following parameters:
- *
- *
data: an object with the property 'selected' that is an array of the primary keys for the selected rows.
- *
response: The XMLHttpResponse object
- *
- * @param {Function} [config.failure] The function to call upon error of the request.
- * The callback will be passed the following parameters:
- *
- *
errorInfo: an object containing detailed error information (may be null)
- *
response: The XMLHttpResponse object
- *
- * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
- * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
- * @param {boolean} [config.clearSelected] If true, clear the session-based selection for this Data Region after
- * retrieving the current selection. Defaults to false.
- *
- * @see LABKEY.DataRegion#setSelected
- * @see LABKEY.DataRegion#clearSelected
- */
- LABKEY.DataRegion.getSelected = function(config) {
- var jsonData = { key: config.selectionKey };
-
- // Issue 41705: Support clearing selection from getSelected()
- if (config.clearSelected) {
- jsonData.clearSelected = true;
- }
-
- LABKEY.Ajax.request({
- url: LABKEY.ActionURL.buildURL('query', 'getSelected.api', config.containerPath),
- method: 'POST',
- jsonData: jsonData,
- success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
- failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
- });
- };
-
- /**
- * MessageArea wraps the display of messages in a DataRegion.
- * @param dataRegion - The dataregion that the MessageArea will bind itself to.
- * @param {String[]} [messages] - An initial messages object containing mappings of 'part' to 'msg'
- * @constructor
- */
- var MessageArea = function(dataRegion, messages) {
- this.bindRegion(dataRegion);
-
- if (messages) {
- this.setMessages(messages);
- }
- };
-
- var MsgProto = MessageArea.prototype;
-
- MsgProto.bindRegion = function(region) {
- this.parentSel = '#' + region.domId + '-msgbox';
- };
-
- MsgProto.toJSON = function() {
- return this.parts;
- };
-
- MsgProto.addMessage = function(msg, part, append) {
- part = part || 'info';
-
- var p = part.toLowerCase();
- if (append && this.parts.hasOwnProperty(p))
- {
- this.parts[p] += msg;
- this.render(p, msg);
- }
- else {
- this.parts[p] = msg;
- this.render(p);
- }
- };
-
- MsgProto.getMessage = function(part) {
- return this.parts[part.toLowerCase()];
- };
-
- MsgProto.hasMessage = function(part) {
- return this.getMessage(part) !== undefined;
- };
-
- MsgProto.hasContent = function() {
- return this.parts && Object.keys(this.parts).length > 0;
- };
-
- MsgProto.removeAll = function() {
- this.parts = {};
- this.render();
- };
-
- MsgProto.removeMessage = function(part) {
- var p = part.toLowerCase();
- if (this.parts.hasOwnProperty(p)) {
- this.parts[p] = undefined;
- this.render();
- }
- };
-
- MsgProto.setMessages = function(messages) {
- if (LABKEY.Utils.isObject(messages)) {
- this.parts = messages;
- }
- else {
- this.parts = {};
- }
- };
-
- MsgProto.getParent = function() {
- return $(this.parentSel);
- };
-
- MsgProto.render = function(partToUpdate, appendMsg) {
- var hasMsg = false,
- me = this,
- parent = this.getParent();
-
- $.each(this.parts, function(part, msg) {
-
- if (msg) {
- // If this is modified, update the server-side renderer in DataRegion.java renderMessages()
- var partEl = parent.find('div[data-msgpart="' + part + '"]');
- if (partEl.length === 0) {
- parent.append([
- '
',
- msg,
- '
'
- ].join(''));
- }
- else if (partToUpdate !== undefined && partToUpdate === part) {
- if (appendMsg !== undefined)
- partEl.append(appendMsg);
- else
- partEl.html(msg)
- }
-
- hasMsg = true;
- }
- else {
- parent.find('div[data-msgpart="' + part + '"]').remove();
- delete me.parts[part];
- }
- });
-
- if (hasMsg) {
- this.show();
- $(this).trigger('rendermsg', [this, this.parts]);
- }
- else {
- this.hide();
- parent.html('');
- }
- };
-
- MsgProto.show = function() { this.getParent().show(); };
- MsgProto.hide = function() { this.getParent().hide(); };
- MsgProto.isVisible = function() { return $(this.parentSel + ':visible').length > 0; };
- MsgProto.find = function(selector) {
- return this.getParent().find('.dataregion_msgbox_ct').find(selector);
- };
- MsgProto.on = function(evt, callback, scope) { $(this).bind(evt, $.proxy(callback, scope)); };
-
- /**
- * @description Constructs a LABKEY.QueryWebPart class instance
- * @class The LABKEY.QueryWebPart simplifies the task of dynamically adding a query web part to your page. Please use
- * this class for adding query web parts to a page instead of {@link LABKEY.WebPart},
- * which can be used for other types of web parts.
- *
- * @constructor
- * @param {Object} config A configuration object with the following possible properties:
- * @param {String} config.schemaName The name of the schema the web part will query.
- * @param {String} config.queryName The name of the query within the schema the web part will select and display.
- * @param {String} [config.viewName] the name of a saved view you wish to display for the given schema and query name.
- * @param {String} [config.reportId] the report id of a saved report you wish to display for the given schema and query name.
- * @param {Mixed} [config.renderTo] The element id, DOM element, or Ext element inside of which the part should be rendered. This is typically a <div>.
- * If not supplied in the configuration, you must call the render() method to render the part into the page.
- * @param {String} [config.errorType] A parameter to specify how query parse errors are returned. (default 'html'). Valid
- * values are either 'html' or 'json'. If 'html' is specified the error will be rendered to an HTML view, if 'json' is specified
- * the errors will be returned to the callback handlers as an array of objects named 'parseErrors' with the following properties:
- *
- *
msg: The error message.
- *
line: The line number the error occurred at (optional).
- *
col: The column number the error occurred at (optional).
- *
errorStr: The line from the source query that caused the error (optional).
- *
- * @param {String} [config.sql] A SQL query that can be used instead of an existing schema name/query name combination.
- * @param {Object} [config.metadata] Metadata that can be applied to the properties of the table fields. Currently, this option is only
- * available if the query has been specified through the config.sql option. For full documentation on
- * available properties, see LabKey XML Schema Reference.
- * This object may contain the following properties:
- *
- *
type: The type of metadata being specified. Currently, only 'xml' is supported.
- *
value: The metadata XML value as a string. For example: '<tables xmlns="http://labkey.org/data/xml"><table tableName="Announcement" tableDbType="NOT_IN_DB"><columns><column columnName="Title"><columnTitle>Custom Title</columnTitle></column></columns></table></tables>'
- *
- * @param {String} [config.title] A title for the web part. If not supplied, the query name will be used as the title.
- * @param {String} [config.titleHref] If supplied, the title will be rendered as a hyperlink with this value as the href attribute.
- * @param {String} [config.buttonBarPosition] DEPRECATED--see config.buttonBar.position
- * @param {boolean} [config.allowChooseQuery] If the button bar is showing, whether or not it should be include a button
- * to let the user choose a different query.
- * @param {boolean} [config.allowChooseView] If the button bar is showing, whether or not it should be include a button
- * to let the user choose a different view.
- * @param {String} [config.detailsURL] Specify or override the default details URL for the table with one of the form
- * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
- * @param {boolean} [config.showDetailsColumn] If the underlying table has a details URL, show a column that renders a [details] link (default true). If true, the record selectors will be included regardless of the 'showRecordSelectors' config option.
- * @param {String} [config.updateURL] Specify or override the default updateURL for the table with one of the form
- * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
- * @param {boolean} [config.showUpdateColumn] If the underlying table has an update URL, show a column that renders an [edit] link (default true).
- * @param {String} [config.insertURL] Specify or override the default insert URL for the table with one of the form
- * "/controller/insertAction.view" or "org.labkey.package.MyController$InsertActionAction.class"
- * @param {String} [config.importURL] Specify or override the default bulk import URL for the table with one of the form
- * "/controller/importAction.view" or "org.labkey.package.MyController$ImportActionAction.class"
- * @param {String} [config.deleteURL] Specify or override the default delete URL for the table with one of the form
- * "/controller/action.view" or "org.labkey.package.MyController$ActionAction.class". The keys for the selected rows
- * will be included in the POST.
- * @param {boolean} [config.showImportDataButton] If the underlying table has an import URL, show an "Import Bulk Data" button in the button bar (default true).
- * @param {boolean} [config.showInsertNewButton] If the underlying table has an insert URL, show an "Insert New" button in the button bar (default true).
- * @param {boolean} [config.showDeleteButton] Show a "Delete" button in the button bar (default true).
- * @param {boolean} [config.showReports] If true, show reports on the Views menu (default true).
- * @param {boolean} [config.showExportButtons] Show the export button menu in the button bar (default true).
- * @param {boolean} [config.showRStudioButton] Show the export to RStudio button menu in the button bar. Requires export button to work. (default false).
- * @param {boolean} [config.showBorders] Render the table with borders (default true).
- * @param {boolean} [config.showSurroundingBorder] Render the table with a surrounding border (default true).
- * @param {boolean} [config.showFilterDescription] Include filter and parameter values in the grid header, if present (default true).
- * @param {boolean} [config.showRecordSelectors] Render the select checkbox column (default undefined, meaning they will be shown if the query is updatable by the current user).
- * Both 'showDeleteButton' and 'showExportButtons' must be set to false for the 'showRecordSelectors = false' setting to hide the checkboxes.
- * @param {boolean} [config.showPagination] Show the pagination links and count (default true).
- * @param {boolean} [config.showPaginationCount] Show the total count of rows in the pagination information text (default true).
- * @param {boolean} [config.showPaginationCountAsync] Show the total count of rows in the pagination information text, but query for it asynchronously so that the grid data can load initially without it (default false).
- * @param {boolean} [config.shadeAlternatingRows] Shade every other row with a light gray background color (default true).
- * @param {boolean} [config.suppressRenderErrors] If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false.
- * @param {Object} [config.buttonBar] Button bar configuration. This object may contain any of the following properties:
- *
- *
position: Configures where the button bar will appear with respect to the data grid: legal values are 'top', or 'none'. Default is 'top'.
- *
includeStandardButtons: If true, all standard buttons not specifically mentioned in the items array will be included at the end of the button bar. Default is false.
- *
items: An array of button bar items. Each item may be either a reference to a standard button, or a new button configuration.
- * to reference standard buttons, use one of the properties on {@link #standardButtons}, or simply include a string
- * that matches the button's caption. To include a new button configuration, create an object with the following properties:
- *
- *
text: The text you want displayed on the button (aka the caption).
- *
url: The URL to navigate to when the button is clicked. You may use LABKEY.ActionURL to build URLs to controller actions.
- * Specify this or a handler function, but not both.
- *
handler: A reference to the JavaScript function you want called when the button is clicked.
- *
permission: Optional. Permission that the current user must possess to see the button.
- * Valid options are 'READ', 'INSERT', 'UPDATE', 'DELETE', and 'ADMIN'.
- * Default is 'READ' if permissionClass is not specified.
- *
permissionClass: Optional. If permission (see above) is not specified, the fully qualified Java class
- * name of the permission that the user must possess to view the button.
- *
requiresSelection: A boolean value (true/false) indicating whether the button should only be enabled when
- * data rows are checked/selected.
- *
items: To create a drop-down menu button, set this to an array of menu item configurations.
- * Each menu item configuration can specify any of the following properties:
- *
- *
text: The text of the menu item.
- *
handler: A reference to the JavaScript function you want called when the menu item is clicked.
- *
icon: A url to an image to use as the menu item's icon.
- *
items: An array of sub-menu item configurations. Used for fly-out menus.
- *
- *
- *
- *
- *
- * @param {String} [config.columns] Comma-separated list of column names to be shown in the grid, overriding
- * whatever might be set in a custom view.
- * @param {String} [config.sort] A base sort order to use. This is a comma-separated list of column names, each of
- * which may have a - prefix to indicate a descending sort. It will be treated as the final sort, after any that the user
- * has defined in a custom view or through interacting with the grid column headers.
- * @param {String} [config.removeableSort] An additional sort order to use. This is a comma-separated list of column names, each of
- * which may have a - prefix to indicate a descending sort. It will be treated as the first sort, before any that the user
- * has defined in a custom view or through interacting with the grid column headers.
- * @param {Array} [config.filters] A base set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
- * each of which is created using the {@link LABKEY.Filter.create} method. These filters cannot be removed by the user
- * interacting with the UI.
- * For compatibility with the {@link LABKEY.Query} object, you may also specify base filters using config.filterArray.
- * @param {Array} [config.removeableFilters] A set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
- * each of which is created using the {@link LABKEY.Filter.create} method. These filters can be modified or removed by the user
- * interacting with the UI.
- * @param {Object} [config.parameters] Map of name (string)/value pairs for the values of parameters if the SQL
- * references underlying queries that are parameterized. For example, the following passes two parameters to the query: {'Gender': 'M', 'CD4': '400'}.
- * The parameters are written to the request URL as follows: query.param.Gender=M&query.param.CD4=400. For details on parameterized SQL queries, see
- * Parameterized SQL Queries.
- * @param {Array} [config.aggregates] An array of aggregate definitions. The objects in this array should have the properties:
- *
- *
column: The name of the column to be aggregated.
- *
type: The aggregate type (see {@link LABKEY.AggregateTypes})
- *
label: Optional label used when rendering the aggregate row.
- *
- * @param {String} [config.showRows] Either 'paginated' (the default) 'selected', 'unselected', 'all', or 'none'.
- * When 'paginated', the maxRows and offset parameters can be used to page through the query's result set rows.
- * When 'selected' or 'unselected' the set of rows selected or unselected by the user in the grid view will be returned.
- * You can programmatically get and set the selection using the {@link LABKEY.DataRegion.setSelected} APIs.
- * Setting config.maxRows to -1 is the same as 'all'
- * and setting config.maxRows to 0 is the same as 'none'.
- * @param {Integer} [config.maxRows] The maximum number of rows to return from the server (defaults to 100).
- * If you want to return all possible rows, set this config property to -1.
- * @param {Integer} [config.offset] The index of the first row to return from the server (defaults to 0).
- * Use this along with the maxRows config property to request pages of data.
- * @param {String} [config.dataRegionName] The name to be used for the data region. This should be unique within
- * the set of query views on the page. If not supplied, a unique name is generated for you.
- * @param {String} [config.linkTarget] The name of a browser window/tab in which to open URLs rendered in the
- * QueryWebPart. If not supplied, links will generally be opened in the same browser window/tab where the QueryWebPart.
- * @param {String} [config.frame] The frame style to use for the web part. This may be one of the following:
- * 'div', 'portal', 'none', 'dialog', 'title', 'left-nav'.
- * @param {String} [config.showViewPanel] Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab".
- * @param {String} [config.bodyClass] A CSS style class that will be added to the enclosing element for the web part.
- * Note, this may not be applied when used in conjunction with some "frame" types (e.g. 'none').
- * @param {Function} [config.success] A function to call after the part has been rendered. It will be passed two arguments:
- *
- *
dataRegion: the LABKEY.DataRegion object representing the rendered QueryWebPart
- *
request: the XMLHTTPRequest that was issued to the server
- *
- * @param {Function} [config.failure] A function to call if the request to retrieve the content fails. It will be passed three arguments:
- *
- *
json: JSON object containing the exception.
- *
response: The XMLHttpRequest object containing the response data.
- *
options: The parameter to the request call.
- *
- * @param {Object} [config.scope] An object to use as the callback function's scope. Defaults to this.
- * @param {int} [config.timeout] A timeout for the AJAX call, in milliseconds. Default is 30000 (30 seconds).
- * @param {String} [config.containerPath] The container path in which the schema and query name are defined. If not supplied, the current container path will be used.
- * @param {String} [config.containerFilter] One of the values of {@link LABKEY.Query.containerFilter} that sets the scope of this query. If not supplied, the current folder will be used.
- * @example
- * <div id='queryTestDiv1'/>
- * <script type="text/javascript">
- var qwp1 = new LABKEY.QueryWebPart({
-
- renderTo: 'queryTestDiv1',
- title: 'My Query Web Part',
- schemaName: 'lists',
- queryName: 'People',
- buttonBarPosition: 'none',
- aggregates: [
- {column: 'First', type: LABKEY.AggregateTypes.COUNT, label: 'Total People'},
- {column: 'Age', type: LABKEY.AggregateTypes.MEAN}
- ],
- filters: [
- LABKEY.Filter.create('Last', 'Flintstone')
- ],
- sort: '-Last'
- });
-
- //note that you may also register for the 'render' event
- //instead of using the success config property.
- //registering for events is done using Ext event registration.
- //Example:
- qwp1.on("render", onRender);
- function onRender()
- {
- //...do something after the part has rendered...
- }
-
- ///////////////////////////////////////
- // Custom Button Bar Example
-
- var qwp1 = new LABKEY.QueryWebPart({
- renderTo: 'queryTestDiv1',
- title: 'My Query Web Part',
- schemaName: 'lists',
- queryName: 'People',
- buttonBar: {
- includeStandardButtons: true,
- items:[
- LABKEY.QueryWebPart.standardButtons.views,
- {text: 'Test', url: LABKEY.ActionURL.buildURL('project', 'begin')},
- {text: 'Test Script', onClick: "alert('Hello World!'); return false;"},
- {text: 'Test Handler', handler: onTestHandler},
- {text: 'Test Menu', items: [
- {text: 'Item 1', handler: onItem1Handler},
- {text: 'Fly Out', items: [
- {text: 'Sub Item 1', handler: onItem1Handler}
- ]},
- '-', //separator
- {text: 'Item 2', handler: onItem2Handler}
- ]},
- LABKEY.QueryWebPart.standardButtons.exportRows
- ]}
- });
-
- function onTestHandler(dataRegion)
- {
- alert("onTestHandler called!");
- return false;
- }
-
- function onItem1Handler(dataRegion)
- {
- alert("onItem1Handler called!");
- }
-
- function onItem2Handler(dataRegion)
- {
- alert("onItem2Handler called!");
- }
-
- </script>
- */
- LABKEY.QueryWebPart = function(config) {
- config._useQWPDefaults = true;
- return LABKEY.DataRegion.create(config);
- };
-})(jQuery);
-
-/**
- * A read-only object that exposes properties representing standard buttons shown in LabKey data grids.
- * These are used in conjunction with the buttonBar configuration. The following buttons are currently defined:
- *
- *
LABKEY.QueryWebPart.standardButtons.query
- *
LABKEY.QueryWebPart.standardButtons.views
- *
LABKEY.QueryWebPart.standardButtons.charts
- *
LABKEY.QueryWebPart.standardButtons.insertNew
- *
LABKEY.QueryWebPart.standardButtons.deleteRows
- *
LABKEY.QueryWebPart.standardButtons.exportRows
- *
LABKEY.QueryWebPart.standardButtons.print
- *
- * @name standardButtons
- * @memberOf LABKEY.QueryWebPart#
- */
-LABKEY.QueryWebPart.standardButtons = {
- query: 'query',
- views: 'grid views',
- charts: 'charts',
- insertNew: 'insert',
- deleteRows: 'delete',
- exportRows: 'export',
- print: 'print'
-};
-
-/**
- * Requests the query web part content and renders it within the element identified by the renderTo parameter.
- * Note that you do not need to call this method explicitly if you specify a renderTo property on the config object
- * handed to the class constructor. If you do not specify renderTo in the config, then you must call this method
- * passing the id of the element in which you want the part rendered
- * @function
- * @param renderTo The id of the element in which you want the part rendered.
- */
-
-LABKEY.QueryWebPart.prototype.render = LABKEY.DataRegion.prototype.render;
-
-/**
- * @returns {LABKEY.DataRegion}
- */
-LABKEY.QueryWebPart.prototype.getDataRegion = LABKEY.DataRegion.prototype.getDataRegion;
-
-LABKEY.AggregateTypes = {
- /**
- * Displays the sum of the values in the specified column
- */
- SUM: 'sum',
- /**
- * Displays the mean of the values in the specified column
- */
- MEAN: 'mean',
- /**
- * Displays the count of the non-blank values in the specified column
- */
- COUNT: 'count',
- /**
- * Displays the maximum value from the specified column
- */
- MIN: 'min',
- /**
- * Displays the minimum values from the specified column
- */
- MAX: 'max',
-
- /**
- * Deprecated
- */
- AVG: 'mean'
-
- // TODO how to allow premium module additions to aggregate types?
-};
+/*
+ * Copyright (c) 2015-2019 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+if (!LABKEY.DataRegions) {
+ LABKEY.DataRegions = {};
+}
+
+(function($) {
+
+ //
+ // CONSTANTS
+ //
+ // Issue 48715: Limit the number of rows that can be displayed in a data region
+ var ALL_ROWS_MAX = 5_000;
+ var CUSTOM_VIEW_PANELID = '~~customizeView~~';
+ var DEFAULT_TIMEOUT = 30_000;
+ const MAX_SELECTION_SIZE = 1_000;
+ var PARAM_PREFIX = '.param.';
+ var SORT_ASC = '+';
+ var SORT_DESC = '-';
+
+ //
+ // URL PREFIXES
+ //
+ var ALL_FILTERS_SKIP_PREFIX = '.~';
+ var COLUMNS_PREFIX = '.columns';
+ var CONTAINER_FILTER_NAME = '.containerFilterName';
+ var MAX_ROWS_PREFIX = '.maxRows';
+ var OFFSET_PREFIX = '.offset';
+ var REPORTID_PREFIX = '.reportId';
+ var SORT_PREFIX = '.sort';
+ var SHOW_ROWS_PREFIX = '.showRows';
+ var VIEWNAME_PREFIX = '.viewName';
+
+ // Issue 33536: These prefixes should match the URL parameter key exactly
+ var EXACT_MATCH_PREFIXES = [
+ COLUMNS_PREFIX,
+ CONTAINER_FILTER_NAME,
+ MAX_ROWS_PREFIX,
+ OFFSET_PREFIX,
+ REPORTID_PREFIX,
+ SORT_PREFIX,
+ SHOW_ROWS_PREFIX,
+ VIEWNAME_PREFIX
+ ];
+
+ var VALID_LISTENERS = [
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name afterpanelhide
+ * @event LABKEY.DataRegion.prototype#hidePanel
+ * @description Fires after hiding a visible 'Customize Grid' panel.
+ */
+ 'afterpanelhide',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name afterpanelshow
+ * @event LABKEY.DataRegion.prototype.showPanel
+ * @description Fires after showing 'Customize Grid' panel.
+ */
+ 'afterpanelshow',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforechangeview
+ * @event
+ * @description Fires before changing grid/view/report.
+ * @see LABKEY.DataRegion#changeView
+ */
+ 'beforechangeview',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforeclearsort
+ * @event
+ * @description Fires before clearing sort applied to grid.
+ * @see LABKEY.DataRegion#clearSort
+ */
+ 'beforeclearsort',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforemaxrowschange
+ * @event
+ * @description Fires before change page size.
+ * @see LABKEY.DataRegion#setMaxRows
+ */
+ 'beforemaxrowschange',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforeoffsetchange
+ * @event
+ * @description Fires before change page number.
+ * @see LABKEY.DataRegion#setPageOffset
+ */
+ 'beforeoffsetchange',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforerefresh
+ * @event
+ * @description Fires before refresh grid.
+ * @see LABKEY.DataRegion#refresh
+ */
+ 'beforerefresh',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforesetparameters
+ * @event
+ * @description Fires before setting the parameterized query values for this query.
+ * @see LABKEY.DataRegion#setParameters
+ */
+ 'beforesetparameters',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name beforesortchange
+ * @event
+ * @description Fires before change sorting on the grid.
+ * @see LABKEY.DataRegion#changeSort
+ */
+ 'beforesortchange',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @member
+ * @name render
+ * @event
+ * @description Fires when data region renders.
+ */
+ 'render',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name selectchange
+ * @event
+ * @description Fires when data region selection changes.
+ */
+ 'selectchange',
+ /**
+ * @memberOf LABKEY.DataRegion.prototype
+ * @name success
+ * @event
+ * @description Fires when data region loads successfully.
+ */
+ 'success'];
+
+ // TODO: Update constants to not include '.' so mapping can be used easier
+ var REQUIRE_NAME_PREFIX = {
+ '~': true,
+ 'columns': true,
+ 'param': true,
+ 'reportId': true,
+ 'sort': true,
+ 'offset': true,
+ 'maxRows': true,
+ 'showRows': true,
+ 'containerFilterName': true,
+ 'viewName': true,
+ 'disableAnalytics': true
+ };
+
+ //
+ // PRIVATE VARIABLES
+ //
+ var _paneCache = {};
+
+ /**
+ * The DataRegion constructor is private - to get a LABKEY.DataRegion object, use LABKEY.DataRegions['dataregionname'].
+ * @class LABKEY.DataRegion
+ * The DataRegion class allows you to interact with LabKey grids, including querying and modifying selection state, filters, and more.
+ * @constructor
+ */
+ LABKEY.DataRegion = function(config) {
+ _init.call(this, config, true);
+ };
+
+ LABKEY.DataRegion.prototype.toJSON = function() {
+ return {
+ name: this.name,
+ schemaName: this.schemaName,
+ queryName: this.queryName,
+ viewName: this.viewName,
+ offset: this.offset,
+ maxRows: this.maxRows,
+ messages: this.msgbox.toJSON() // hmm, unsure exactly how this works
+ };
+ };
+
+ /**
+ *
+ * @param {Object} config
+ * @param {Boolean} [applyDefaults=false]
+ * @private
+ */
+ var _init = function(config, applyDefaults) {
+
+ // ensure name
+ if (!config.dataRegionName) {
+ if (!config.name) {
+ this.name = LABKEY.Utils.id('aqwp');
+ }
+ else {
+ this.name = config.name;
+ }
+ }
+ else if (!config.name) {
+ this.name = config.dataRegionName;
+ }
+ else {
+ this.name = config.name;
+ }
+
+ if (!this.name) {
+ throw '"name" is required to initialize a LABKEY.DataRegion';
+ }
+
+ // _useQWPDefaults is only used on initial construction
+ var isQWP = config._useQWPDefaults === true;
+ delete config._useQWPDefaults;
+
+ if (config.buttonBar && config.buttonBar.items && LABKEY.Utils.isArray(config.buttonBar.items)) {
+ // Be tolerant of the caller passing in undefined items, as pageSize has been removed as an option. Strip
+ // them out so they don't cause problems downstream. See Issue 34562.
+ config.buttonBar.items = config.buttonBar.items.filter(function (value, index, arr) {
+ return value;
+ });
+ }
+
+ var settings;
+
+ if (applyDefaults) {
+
+ // defensively remove, not allowed to be set
+ delete config._userSort;
+
+ /**
+ * Config Options
+ */
+ var defaults = {
+
+ _allowHeaderLock: isQWP,
+
+ _failure: isQWP ? LABKEY.Utils.getOnFailure(config) : undefined,
+
+ _success: isQWP ? LABKEY.Utils.getOnSuccess(config) : undefined,
+
+ aggregates: undefined,
+
+ allowChooseQuery: undefined,
+
+ allowChooseView: undefined,
+
+ async: isQWP,
+
+ bodyClass: undefined,
+
+ buttonBar: undefined,
+
+ buttonBarPosition: undefined,
+
+ chartWizardURL: undefined,
+
+ /**
+ * All rows visible on the current page.
+ */
+ complete: false,
+
+ /**
+ * The currently applied container filter. Note, this is only if it is set on the URL, otherwise
+ * the containerFilter could come from the view configuration. Use getContainerFilter()
+ * on this object to get the right value.
+ */
+ containerFilter: undefined,
+
+ containerPath: undefined,
+
+ /**
+ * @deprecated use region.name instead
+ */
+ dataRegionName: this.name,
+
+ detailsURL: undefined,
+
+ domId: undefined,
+
+ /**
+ * The faceted filter pane as been loaded
+ * @private
+ */
+ facetLoaded: false,
+
+ filters: undefined,
+
+ frame: isQWP ? undefined : 'none',
+
+ errorType: 'html',
+
+ /**
+ * Id of the DataRegion. Same as name property.
+ */
+ id: this.name,
+
+ deleteURL: undefined,
+
+ importURL: undefined,
+
+ insertURL: undefined,
+
+ linkTarget: undefined,
+
+ /**
+ * Maximum number of rows to be displayed. 0 if the count is not limited. Read-only.
+ */
+ maxRows: 0,
+
+ metadata: undefined,
+
+ /**
+ * Name of the DataRegion. Should be unique within a given page. Read-only. This will also be used as the id.
+ */
+ name: this.name,
+
+ /**
+ * The index of the first row to return from the server (defaults to 0). Use this along with the maxRows config property to request pages of data.
+ */
+ offset: 0,
+
+ parameters: undefined,
+
+ /**
+ * Name of the query to which this DataRegion is bound. Read-only.
+ */
+ queryName: '',
+
+ disableAnalytics: false,
+
+ removeableContainerFilter: undefined,
+
+ removeableFilters: undefined,
+
+ removeableSort: undefined,
+
+ renderTo: undefined,
+
+ reportId: undefined,
+
+ requestURL: isQWP ? window.location.href : (document.location.search.substring(1) /* strip the ? */ || ''),
+
+ returnUrl: isQWP ? window.location.href : undefined,
+
+ /**
+ * Schema name of the query to which this DataRegion is bound. Read-only.
+ */
+ schemaName: '',
+
+ /**
+ * An object to use as the callback function's scope. Defaults to this.
+ */
+ scope: this,
+
+ /**
+ * URL to use when selecting all rows in the grid. May be null. Read-only.
+ */
+ selectAllURL: undefined,
+
+ selectedCount: 0,
+
+ shadeAlternatingRows: undefined,
+
+ showBorders: undefined,
+
+ showDeleteButton: undefined,
+
+ showDetailsColumn: undefined,
+
+ showExportButtons: undefined,
+
+ showRStudioButton: undefined,
+
+ showImportDataButton: undefined,
+
+ showInsertNewButton: undefined,
+
+ showPagination: undefined,
+
+ showPaginationCount: undefined,
+
+ showPaginationCountAsync: false,
+
+ showRecordSelectors: false,
+
+ showFilterDescription: true,
+
+ showReports: undefined,
+
+ /**
+ * An enum declaring which set of rows to show. all | selected | unselected | paginated
+ */
+ showRows: 'paginated',
+
+ showSurroundingBorder: undefined,
+
+ showUpdateColumn: undefined,
+
+ /**
+ * Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab".
+ */
+ showViewPanel: undefined,
+
+ sort: undefined,
+
+ sql: undefined,
+
+ /**
+ * If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false.
+ */
+ suppressRenderErrors: false,
+
+ /**
+ * A timeout for the AJAX call, in milliseconds.
+ */
+ timeout: undefined,
+
+ title: undefined,
+
+ titleHref: undefined,
+
+ totalRows: undefined, // totalRows isn't available when showing all rows.
+
+ updateURL: undefined,
+
+ userContainerFilter: undefined, // TODO: Incorporate this with the standard containerFilter
+
+ userFilters: {},
+
+ /**
+ * Name of the custom view to which this DataRegion is bound, may be blank. Read-only.
+ */
+ viewName: null
+ };
+
+ settings = $.extend({}, defaults, config);
+ }
+ else {
+ settings = $.extend({}, config);
+ }
+
+ // if showPaginationCountAsync is set to true, make sure that showPaginationCount is false
+ if (settings.showPaginationCountAsync && settings.showPaginationCount) {
+ settings.showPaginationCount = false;
+ }
+
+ // if 'filters' is not specified and 'filterArray' is, use 'filterArray'
+ if (!LABKEY.Utils.isArray(settings.filters) && LABKEY.Utils.isArray(config.filterArray)) {
+ settings.filters = config.filterArray;
+ }
+
+ // Any 'key' of this object will not be copied from settings to the region instance
+ var blackList = {
+ failure: true,
+ success: true
+ };
+
+ for (var s in settings) {
+ if (settings.hasOwnProperty(s) && !blackList[s]) {
+ this[s] = settings[s];
+ }
+ }
+
+ if (config.renderTo) {
+ _convertRenderTo(this, config.renderTo);
+ }
+
+ if (LABKEY.Utils.isArray(this.removeableFilters)) {
+ LABKEY.Filter.appendFilterParams(this.userFilters, this.removeableFilters, this.name);
+ delete this.removeableFilters; // they've been applied
+ }
+
+ // initialize sorting
+ if (this._userSort === undefined) {
+ this._userSort = _getUserSort(this, true /* asString */);
+ }
+
+ if (LABKEY.Utils.isString(this.removeableSort)) {
+ this._userSort = this.removeableSort + (this._userSort ? this._userSort : '');
+ delete this.removeableSort;
+ }
+
+ this._allowHeaderLock = this.allowHeaderLock === true;
+
+ if (!config.messages) {
+ this.messages = {};
+ }
+
+ /**
+ * @ignore
+ * Non-configurable Options
+ */
+ this.selectionModified = false;
+
+ if (this.panelConfigurations === undefined) {
+ this.panelConfigurations = {};
+ }
+
+ if (isQWP && this.renderTo) {
+ _load(this);
+ }
+ else if (!isQWP) {
+ _initContexts.call(this);
+ _initMessaging.call(this);
+ _initSelection.call(this);
+ _initPaging.call(this);
+ _initHeaderLocking.call(this);
+ _initCustomViews.call(this);
+ _initPanes.call(this);
+ _initReport.call(this);
+ }
+ // else the user needs to call render
+
+ // bind supported listeners
+ if (isQWP) {
+ var me = this;
+ if (config.listeners) {
+ var scope = config.listeners.scope || me;
+ $.each(config.listeners, function(event, handler) {
+ if ($.inArray(event, VALID_LISTENERS) > -1) {
+
+ // support either "event: function" or "event: { fn: function }"
+ var callback;
+ if ($.isFunction(handler)) {
+ callback = handler;
+ }
+ else if ($.isFunction(handler.fn)) {
+ callback = handler.fn;
+ }
+ else {
+ throw 'Unsupported listener configuration: ' + event;
+ }
+
+ $(me).bind(event, function() {
+ callback.apply(scope, $(arguments).slice(1));
+ });
+ }
+ else if (event != 'scope') {
+ throw 'Unsupported listener: ' + event;
+ }
+ });
+ }
+ }
+ };
+
+ LABKEY.DataRegion.prototype.destroy = function() {
+ // clean-up panel configurations because we preserve this in init
+ this.panelConfigurations = {};
+
+ // currently a no-op, but should be used to clean-up after ourselves
+ this.disableHeaderLock();
+ };
+
+ /**
+ * Refreshes the grid, via AJAX region is in async mode (loaded through a QueryWebPart),
+ * and via a page reload otherwise. Can be prevented with a listener
+ * on the 'beforerefresh'
+ * event.
+ */
+ LABKEY.DataRegion.prototype.refresh = function() {
+ $(this).trigger('beforerefresh', this);
+
+ if (this.async) {
+ _load(this);
+ }
+ else {
+ window.location.reload();
+ }
+ };
+
+ //
+ // Filtering
+ //
+
+ /**
+ * Add a filter to this Data Region.
+ * @param {LABKEY.Filter} filter
+ * @see LABKEY.DataRegion.addFilter static method.
+ */
+ LABKEY.DataRegion.prototype.addFilter = function(filter) {
+ this.clearSelected({quiet: true});
+ _updateFilter(this, filter);
+ };
+
+ /**
+ * Removes all filters from the DataRegion
+ */
+ LABKEY.DataRegion.prototype.clearAllFilters = function() {
+ this.clearSelected({quiet: true});
+ if (this.async) {
+ this.offset = 0;
+ this.userFilters = {};
+ }
+
+ _removeParameters(this, [ALL_FILTERS_SKIP_PREFIX, OFFSET_PREFIX]);
+ };
+
+ /**
+ * Removes all the filters for a particular field
+ * @param {string|FieldKey} fieldKey the name of the field from which all filters should be removed
+ */
+ LABKEY.DataRegion.prototype.clearFilter = function(fieldKey) {
+ this.clearSelected({quiet: true});
+ var fk = _resolveFieldKey(this, fieldKey);
+
+ if (fk) {
+ var columnPrefix = '.' + fk.toString() + '~';
+
+ if (this.async) {
+ this.offset = 0;
+
+ if (this.userFilters) {
+ var namePrefix = this.name + columnPrefix,
+ me = this;
+
+ $.each(this.userFilters, function(name, v) {
+ if (name.indexOf(namePrefix) >= 0) {
+ delete me.userFilters[name];
+ }
+ });
+ }
+ }
+
+ _removeParameters(this, [columnPrefix, OFFSET_PREFIX]);
+ }
+ };
+
+ /**
+ * Returns an Array of LABKEY.Filter instances applied when creating this DataRegion. These cannot be removed through the UI.
+ * @returns {Array} Array of {@link LABKEY.Filter} objects that represent currently applied base filters.
+ */
+ LABKEY.DataRegion.prototype.getBaseFilters = function() {
+ if (this.filters) {
+ return this.filters.slice();
+ }
+
+ return [];
+ };
+
+ /**
+ * Returns the {@link LABKEY.Query.containerFilter} currently applied to the DataRegion. Defaults to LABKEY.Query.containerFilter.current.
+ * @returns {String} The container filter currently applied to this DataRegion. Defaults to 'undefined' if a container filter is not specified by the configuration.
+ * @see LABKEY.DataRegion#getUserContainerFilter to get the containerFilter value from the URL.
+ */
+ LABKEY.DataRegion.prototype.getContainerFilter = function() {
+ var cf;
+
+ if (LABKEY.Utils.isString(this.containerFilter) && this.containerFilter.length > 0) {
+ cf = this.containerFilter;
+ }
+ else if (LABKEY.Utils.isObject(this.view) && LABKEY.Utils.isString(this.view.containerFilter) && this.view.containerFilter.length > 0) {
+ cf = this.view.containerFilter;
+ }
+
+ return cf;
+ };
+
+ LABKEY.DataRegion.prototype.getDataRegion = function() {
+ return this;
+ };
+
+ /**
+ * Returns the user {@link LABKEY.Query.containerFilter} parameter from the URL.
+ * @returns {LABKEY.Query.containerFilter} The user container filter.
+ */
+ LABKEY.DataRegion.prototype.getUserContainerFilter = function() {
+ return this.getParameter(this.name + CONTAINER_FILTER_NAME);
+ };
+
+ /**
+ * Returns the user filter from the URL. The filter is represented as an Array of objects of the form:
+ *
+ *
fieldKey: {String} The field key of the filter.
+ *
op: {String} The filter operator (eg. "eq" or "in")
+ *
value: {String} Optional value to filter by.
+ *
+ * @returns {Object} Object representing the user filter.
+ * @deprecated 12.2 Use getUserFilterArray instead
+ */
+ LABKEY.DataRegion.prototype.getUserFilter = function() {
+
+ if (LABKEY.devMode) {
+ console.warn([
+ 'LABKEY.DataRegion.getUserFilter() is deprecated since release 12.2.',
+ 'Consider using getUserFilterArray() instead.'
+ ].join(' '));
+ }
+
+ return this.getUserFilterArray().map(function(filter) {
+ return {
+ fieldKey: filter.getColumnName(),
+ op: filter.getFilterType().getURLSuffix(),
+ value: filter.getValue()
+ };
+ });
+ };
+
+ /**
+ * Returns an Array of LABKEY.Filter instances constructed from the URL.
+ * @returns {Array} Array of {@link LABKEY.Filter} objects that represent currently applied filters.
+ */
+ LABKEY.DataRegion.prototype.getUserFilterArray = function() {
+ var userFilter = [], me = this;
+
+ _getParameters(this).forEach(function(pair) {
+ if (pair[0].indexOf(me.name + '.') == 0 && pair[0].indexOf('~') > -1) {
+ var tilde = pair[0].indexOf('~');
+ var fieldKey = pair[0].substring(me.name.length + 1, tilde);
+ var op = pair[0].substring(tilde + 1);
+ userFilter.push(LABKEY.Filter.create(fieldKey, pair[1], LABKEY.Filter.getFilterTypeForURLSuffix(op)));
+ }
+ });
+
+ return userFilter;
+ };
+
+ /**
+ * Remove a filter on this DataRegion.
+ * @param {LABKEY.Filter} filter
+ */
+ LABKEY.DataRegion.prototype.removeFilter = function(filter) {
+ this.clearSelected({quiet: true});
+ if (LABKEY.Utils.isObject(filter) && LABKEY.Utils.isFunction(filter.getColumnName)) {
+ _updateFilter(this, null, [this.name + '.' + filter.getColumnName() + '~']);
+ }
+ };
+
+ /**
+ * Replace a filter on this Data Region. Optionally, supply another filter to replace for cases when the filter
+ * columns don't match exactly.
+ * @param {LABKEY.Filter} filter
+ * @param {LABKEY.Filter} [filterToReplace]
+ */
+ LABKEY.DataRegion.prototype.replaceFilter = function(filter, filterToReplace) {
+ this.clearSelected({quiet: true});
+ var target = filterToReplace ? filterToReplace : filter;
+ _updateFilter(this, filter, [this.name + '.' + target.getColumnName() + '~']);
+ };
+
+ /**
+ * @ignore
+ * @param filters
+ * @param columnNames
+ */
+ LABKEY.DataRegion.prototype.replaceFilters = function(filters, columnNames) {
+ this.clearSelected({quiet: true});
+ var filterPrefixes = [],
+ filterParams = [],
+ me = this;
+
+ if (LABKEY.Utils.isArray(filters)) {
+ filters.forEach(function(filter) {
+ filterPrefixes.push(me.name + '.' + filter.getColumnName() + '~');
+ filterParams.push([filter.getURLParameterName(me.name), filter.getURLParameterValue()]);
+ });
+ }
+
+ var fieldKeys = [];
+
+ if (LABKEY.Utils.isArray(columnNames)) {
+ fieldKeys = fieldKeys.concat(columnNames);
+ }
+ else if ($.isPlainObject(columnNames) && columnNames.fieldKey) {
+ fieldKeys.push(columnNames.fieldKey.toString());
+ }
+
+ // support fieldKeys (e.g. ["ColumnA", "ColumnA/Sub1"])
+ // A special case of fieldKey is "SUBJECT_PREFIX/", used by participant group facet
+ if (fieldKeys.length > 0) {
+ _getParameters(this).forEach(function(param) {
+ var p = param[0];
+ if (p.indexOf(me.name + '.') === 0 && p.indexOf('~') > -1) {
+ $.each(fieldKeys, function(j, name) {
+ var postfix = name && name.length && name[name.length - 1] == '/' ? '' : '~';
+ if (p.indexOf(me.name + '.' + name + postfix) > -1) {
+ filterPrefixes.push(p);
+ }
+ });
+ }
+ });
+ }
+
+ _setParameters(this, filterParams, [OFFSET_PREFIX].concat($.unique(filterPrefixes)));
+ };
+
+ /**
+ * @private
+ * @param filter
+ * @param filterMatch
+ */
+ LABKEY.DataRegion.prototype.replaceFilterMatch = function(filter, filterMatch) {
+ this.clearSelected({quiet: true});
+ var skips = [], me = this;
+
+ _getParameters(this).forEach(function(param) {
+ if (param[0].indexOf(me.name + '.') === 0 && param[0].indexOf(filterMatch) > -1) {
+ skips.push(param[0]);
+ }
+ });
+
+ _updateFilter(this, filter, skips);
+ };
+
+ //
+ // Selection
+ //
+
+ /**
+ * @private
+ */
+ var _initSelection = function() {
+
+ var me = this,
+ form = _getFormSelector(this);
+
+ if (form && form.length) {
+ // backwards compatibility -- some references use this directly
+ // if you're looking to use this internally to the region use _getFormSelector() instead
+ this.form = form[0];
+ }
+
+ if (form && this.showRecordSelectors) {
+ _onSelectionChange(this);
+ }
+
+ // Bind Events
+ _getAllRowSelectors(this).on('click', function(evt) {
+ evt.stopPropagation();
+ me.selectPage.call(me, this.checked);
+ });
+ _getRowSelectors(this).on('click', function() { me.selectRow.call(me, this); });
+
+ // click row highlight
+ var rows = form.find('.labkey-data-region > tbody > tr');
+ rows.on('click', function(e) {
+ if (e.target && e.target.tagName.toLowerCase() === 'td') {
+ $(this).siblings('tr').removeClass('lk-row-hl');
+ $(this).addClass('lk-row-hl');
+ _selClickLock = me;
+ }
+ });
+ rows.on('mouseenter', function() {
+ $(this).siblings('tr').removeClass('lk-row-over');
+ $(this).addClass('lk-row-over');
+ });
+ rows.on('mouseleave', function() {
+ $(this).removeClass('lk-row-over');
+ });
+
+ if (!_selDocClick) {
+ _selDocClick = $(document).on('click', _onDocumentClick);
+ }
+ };
+
+ var _selClickLock; // lock to prevent removing a row highlight that was just applied
+ var _selDocClick; // global (shared across all Data Region instances) click event handler instance
+
+ // Issue 32898: Clear row highlights on document click
+ var _onDocumentClick = function() {
+ if (_selClickLock) {
+ var form = _getFormSelector(_selClickLock);
+ _selClickLock = undefined;
+
+ $('.lk-row-hl').each(function() {
+ if (!form.has($(this)).length) {
+ $(this).removeClass('lk-row-hl');
+ }
+ });
+ }
+ else {
+ $('.lk-row-hl').removeClass('lk-row-hl');
+ }
+ };
+
+ /**
+ * Clear all selected items for the current DataRegion.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {Function} config.success The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'count' of 0 to indicate an empty selection.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ *
+ * @see LABKEY.DataRegion#selectPage
+ * @see LABKEY.DataRegion.clearSelected static method.
+ */
+ LABKEY.DataRegion.prototype.clearSelected = function(config) {
+ config = config || {};
+ config.selectionKey = this.selectionKey;
+ config.scope = config.scope || this;
+
+ this.selectedCount = 0;
+ if (!config.quiet)
+ {
+ _onSelectionChange(this);
+ }
+
+ if (config.selectionKey) {
+ LABKEY.DataRegion.clearSelected(config);
+ }
+
+ if (this.showRows == 'selected') {
+ _removeParameters(this, [SHOW_ROWS_PREFIX]);
+ }
+ else if (this.showRows == 'unselected') {
+ // keep "SHOW_ROWS_PREFIX=unselected" parameter
+ window.location.reload(true);
+ }
+ else {
+ _toggleAllRows(this, false);
+ this.removeMessage('selection');
+ }
+ };
+
+ /**
+ * Get selected items on the current page of the DataRegion, based on the current state of the checkboxes in the
+ * browser's DOM. Note, if the region is paginated, selected items may exist on other pages which will not be
+ * included in the results of this function.
+ * @see LABKEY.DataRegion#getSelected
+ */
+ LABKEY.DataRegion.prototype.getChecked = function() {
+ var values = [];
+ _getRowSelectors(this).each(function() {
+ if (this.checked) {
+ values.push(this.value);
+ }
+ });
+ return values;
+ };
+
+ /**
+ * Get all selected items for this DataRegion, as maintained in server-state. This will include rows on any
+ * pages of a paginated grid, and may not correspond directly with the state of the checkboxes in the current
+ * browser window's DOM if the server-side state has been modified.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {Function} config.success The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'selected' that is an array of the primary keys for the selected rows.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ *
+ * @see LABKEY.DataRegion.getSelected static method.
+ */
+ LABKEY.DataRegion.prototype.getSelected = function(config) {
+ if (!this.selectionKey)
+ return;
+
+ config = config || {};
+ config.selectionKey = this.selectionKey;
+ LABKEY.DataRegion.getSelected(config);
+ };
+
+ /**
+ * Returns the number of selected rows on the current page of the DataRegion. Selected items may exist on other pages.
+ * @returns {Integer} the number of selected rows on the current page of the DataRegion.
+ * @see LABKEY.DataRegion#getSelected to get all selected rows.
+ */
+ LABKEY.DataRegion.prototype.getSelectionCount = function() {
+ if (!$('#' + this.domId)) {
+ return 0;
+ }
+
+ var count = 0;
+ _getRowSelectors(this).each(function() {
+ if (this.checked === true) {
+ count++;
+ }
+ });
+
+ return count;
+ };
+
+ /**
+ * Returns true if any row is checked on the current page of the DataRegion. Selected items may exist on other pages.
+ * @returns {Boolean} true if any row is checked on the current page of the DataRegion.
+ * @see LABKEY.DataRegion#getSelected to get all selected rows.
+ */
+ LABKEY.DataRegion.prototype.hasSelected = function() {
+ return this.getSelectionCount() > 0;
+ };
+
+ /**
+ * Returns true if all rows are checked on the current page of the DataRegion and at least one row is present.
+ * @returns {Boolean} true if all rows are checked on the current page of the DataRegion and at least one row is present.
+ * @see LABKEY.DataRegion#getSelected to get all selected rows.
+ */
+ LABKEY.DataRegion.prototype.isPageSelected = function() {
+ var checkboxes = _getRowSelectors(this);
+ var i=0;
+
+ for (; i < checkboxes.length; i++) {
+ if (!checkboxes[i].checked) {
+ return false;
+ }
+ }
+ return i > 0;
+ };
+
+ LABKEY.DataRegion.prototype.selectAll = function(config) {
+ if (this.selectionKey) {
+ config = config || {};
+ config.scope = config.scope || this;
+
+ // Either use the selectAllURL provided or create a query config
+ // object that can be used with the generic query/selectAll.api action.
+ if (this.selectAllURL) {
+ config.url = this.selectAllURL;
+ }
+ else {
+ config = LABKEY.Utils.apply(config, this.getQueryConfig());
+ }
+
+ config = _chainSelectionCountCallback(this, config);
+
+ LABKEY.DataRegion.selectAll(config);
+
+ if (this.showRows === "selected") {
+ // keep "SHOW_ROWS_PREFIX=selected" parameter
+ window.location.reload(true);
+ }
+ else if (this.showRows === "unselected") {
+ _removeParameters(this, [SHOW_ROWS_PREFIX]);
+ }
+ else {
+ _toggleAllRows(this, true);
+ }
+ }
+ };
+
+ /**
+ * @deprecated use clearSelected instead
+ * @function
+ * @see LABKEY.DataRegion#clearSelected
+ */
+ LABKEY.DataRegion.prototype.selectNone = LABKEY.DataRegion.prototype.clearSelected;
+
+ /**
+ * Set the selection state for all checkboxes on the current page of the DataRegion.
+ * @param checked whether all of the rows on the current page should be selected or unselected
+ * @returns {Array} Array of ids that were selected or unselected.
+ *
+ * @see LABKEY.DataRegion#setSelected to set selected items on the current page of the DataRegion.
+ * @see LABKEY.DataRegion#clearSelected to clear all selected.
+ */
+ LABKEY.DataRegion.prototype.selectPage = function(checked) {
+ var _check = (checked === true);
+ var ids = _toggleAllRows(this, _check);
+ var me = this;
+
+ if (ids.length > 0) {
+ _getAllRowSelectors(this).each(function() { this.checked = _check});
+ this.setSelected({
+ ids: ids,
+ checked: _check,
+ success: function(data) {
+ if (data && data.count > 0 && !this.complete) {
+ var count = data.count;
+ var msg;
+ if (me.totalRows) {
+ if (count == me.totalRows) {
+ msg = 'All ' + this.totalRows + ' rows selected.';
+ }
+ else {
+ msg = 'Selected ' + count + ' of ' + this.totalRows + ' rows.';
+ }
+ }
+ else {
+ // totalRows isn't available when showing all rows.
+ msg = 'Selected ' + count + ' rows.';
+ }
+ _showSelectMessage(me, msg);
+ }
+ else {
+ this.removeMessage('selection');
+ }
+ }
+ });
+ }
+
+ return ids;
+ };
+
+ /**
+ * @ignore
+ * @param el
+ */
+ LABKEY.DataRegion.prototype.selectRow = function(el) {
+ this.setSelected({
+ ids: [el.value],
+ checked: el.checked
+ });
+
+ if (!el.checked) {
+ this.removeMessage('selection');
+ }
+ };
+
+ /**
+ * Add or remove items from the selection associated with this DataRegion.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {Array} config.ids Array of primary key ids for each row to select/unselect.
+ * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected.
+ * @param {Function} [config.success] The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'count' to indicate the updated selection count.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ *
+ * @see LABKEY.DataRegion#getSelected to get the selected items for this DataRegion.
+ * @see LABKEY.DataRegion#clearSelected to clear all selected items for this DataRegion.
+ */
+ LABKEY.DataRegion.prototype.setSelected = function(config) {
+ if (!config || !LABKEY.Utils.isArray(config.ids) || config.ids.length === 0) {
+ return;
+ }
+
+ var me = this;
+ config = config || {};
+ config.selectionKey = this.selectionKey;
+ config.scope = config.scope || me;
+
+ config = _chainSelectionCountCallback(this, config);
+
+ 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');
+ };
+ }
+
+ if (config.selectionKey) {
+ LABKEY.DataRegion.setSelected(config);
+ }
+ else if ($.isFunction(config.success)) {
+ // Don't send the selection change to the server if there is no selectionKey.
+ // Call the success callback directly.
+ config.success.call(config.scope, {count: this.getSelectionCount()});
+ }
+ };
+
+ //
+ // Parameters
+ //
+
+ /**
+ * Removes all parameters from the DataRegion
+ */
+ LABKEY.DataRegion.prototype.clearAllParameters = function() {
+ if (this.async) {
+ this.offset = 0;
+ this.parameters = undefined;
+ }
+
+ _removeParameters(this, [PARAM_PREFIX, OFFSET_PREFIX]);
+ };
+
+ /**
+ * Returns the specified parameter from the URL. Note, this is not related specifically
+ * to parameterized query values (e.g. setParameters()/getParameters())
+ * @param {String} paramName
+ * @returns {*}
+ */
+ LABKEY.DataRegion.prototype.getParameter = function(paramName) {
+ var param = null;
+
+ $.each(_getParameters(this), function(i, pair) {
+ if (pair.length > 0 && pair[0] === paramName) {
+ param = pair.length > 1 ? pair[1] : '';
+ return false;
+ }
+ });
+
+ return param;
+ };
+
+ /**
+ * Get the parameterized query values for this query. These parameters
+ * are named by the query itself.
+ * @param {boolean} toLowercase If true, all parameter names will be converted to lowercase
+ * returns params An Object of key/val pairs.
+ */
+ LABKEY.DataRegion.prototype.getParameters = function(toLowercase) {
+
+ var params = this.parameters ? this.parameters : {},
+ re = new RegExp('^' + LABKEY.Utils.escapeRe(this.name) + LABKEY.Utils.escapeRe(PARAM_PREFIX), 'i'),
+ name;
+
+ _getParameters(this).forEach(function(pair) {
+ if (pair.length > 0 && pair[0].match(re)) {
+ name = pair[0].replace(re, '');
+ if (toLowercase === true) {
+ name = name.toLowerCase();
+ }
+
+ // URL parameters will override this.parameters values
+ params[name] = pair[1];
+ }
+ });
+
+ return params;
+ };
+
+ /**
+ * Set the parameterized query values for this query. These parameters
+ * are named by the query itself.
+ * @param {Mixed} params An Object or Array of Array key/val pairs.
+ */
+ LABKEY.DataRegion.prototype.setParameters = function(params) {
+ var event = $.Event('beforesetparameters');
+
+ $(this).trigger(event);
+
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+
+ var paramPrefix = this.name + PARAM_PREFIX, _params = [];
+ var newParameters = this.parameters ? this.parameters : {};
+
+ function applyParameters(pKey, pValue) {
+ var key = pKey;
+ if (pKey.indexOf(paramPrefix) !== 0) {
+ key = paramPrefix + pKey;
+ }
+ newParameters[key.replace(paramPrefix, '')] = pValue;
+ _params.push([key, pValue]);
+ }
+
+ // convert Object into Array of Array pairs and prefix the parameter name if necessary.
+ if (LABKEY.Utils.isObject(params)) {
+ $.each(params, applyParameters);
+ }
+ else if (LABKEY.Utils.isArray(params)) {
+ params.forEach(function(pair) {
+ if (LABKEY.Utils.isArray(pair) && pair.length > 1) {
+ applyParameters(pair[0], pair[1]);
+ }
+ });
+ }
+ else {
+ return; // invalid argument shape
+ }
+
+ this.parameters = newParameters;
+
+ _setParameters(this, _params, [PARAM_PREFIX, OFFSET_PREFIX]);
+ };
+
+ /**
+ * @ignore
+ * @Deprecated
+ */
+ LABKEY.DataRegion.prototype.setSearchString = function(regionName, search) {
+ this.savedSearchString = search || "";
+ // If the search string doesn't change and there is a hash on the url, the page won't reload.
+ // Remove the hash by setting the full path plus search string.
+ window.location.assign(window.location.pathname + (this.savedSearchString.length > 0 ? "?" + this.savedSearchString : ""));
+ };
+
+ //
+ // Messaging
+ //
+
+ /**
+ * @private
+ */
+ var _initMessaging = function() {
+ if (!this.msgbox) {
+ this.msgbox = new MessageArea(this);
+ this.msgbox.on('rendermsg', function(evt, msgArea, parts) { _onRenderMessageArea(this, parts); }, this);
+ }
+ else {
+ this.msgbox.bindRegion(this);
+ }
+
+ if (this.messages) {
+ this.msgbox.setMessages(this.messages);
+ this.msgbox.render();
+ }
+ };
+
+ /**
+ * Show a message in the header of this DataRegion.
+ * @param {String / Object} config the HTML source of the message to be shown or a config object with the following properties:
+ *
+ *
html: {String} the HTML source of the message to be shown.
+ *
part: {String} The part of the message area to render the message to.
+ *
duration: {Integer} The amount of time (in milliseconds) the message will stay visible.
+ *
hideButtonPanel: {Boolean} If true the button panel (customize view, export, etc.) will be hidden if visible.
+ *
append: {Boolean} If true the msg is appended to any existing content for the given part.
+ *
+ * @param part The part of the message area to render the message to. Used to scope messages so they can be added
+ * and removed without clearing other messages.
+ */
+ LABKEY.DataRegion.prototype.addMessage = function(config, part) {
+ this.hidePanel();
+
+ if (LABKEY.Utils.isString(config)) {
+ this.msgbox.addMessage(config, part);
+ }
+ else if (LABKEY.Utils.isObject(config)) {
+ this.msgbox.addMessage(config.html, config.part || part, config.append);
+
+ if (config.hideButtonPanel) {
+ this.hideButtonPanel();
+ }
+
+ if (config.duration) {
+ var dr = this;
+ setTimeout(function() {
+ dr.removeMessage(config.part || part);
+ _getHeaderSelector(dr).trigger('resize');
+ }, config.duration);
+ }
+ }
+ };
+
+ /**
+ * Clear the message box contents.
+ */
+ LABKEY.DataRegion.prototype.clearMessage = function() {
+ if (this.msgbox) this.msgbox.removeAll();
+ };
+
+ /**
+ * @param part The part of the message area to render the message to. Used to scope messages so they can be added
+ * and removed without clearing other messages.
+ * @return {String} The message for 'part'. Could be undefined.
+ */
+ LABKEY.DataRegion.prototype.getMessage = function(part) {
+ if (this.msgbox) { return this.msgbox.getMessage(part); } // else undefined
+ };
+
+ /**
+ * @param part The part of the message area to render the message to. Used to scope messages so they can be added
+ * and removed without clearing other messages.
+ * @return {Boolean} true iff there is a message area for this region and it has the message keyed by 'part'.
+ */
+ LABKEY.DataRegion.prototype.hasMessage = function(part) {
+ return this.msgbox && this.msgbox.hasMessage(part);
+ };
+
+ LABKEY.DataRegion.prototype.hideContext = function() {
+ _getContextBarSelector(this).hide();
+ _getViewBarSelector(this).hide();
+ };
+
+ /**
+ * If a message is currently showing, hide it and clear out its contents
+ * @param keepContent If true don't remove the message area content
+ */
+ LABKEY.DataRegion.prototype.hideMessage = function(keepContent) {
+ if (this.msgbox) {
+ this.msgbox.hide();
+
+ if (!keepContent)
+ this.removeAllMessages();
+ }
+ };
+
+ /**
+ * Returns true if a message is currently being shown for this DataRegion. Messages are shown as a header.
+ * @return {Boolean} true if a message is showing.
+ */
+ LABKEY.DataRegion.prototype.isMessageShowing = function() {
+ return this.msgbox && this.msgbox.isVisible();
+ };
+
+ /**
+ * Removes all messages from this Data Region.
+ */
+ LABKEY.DataRegion.prototype.removeAllMessages = function() {
+ if (this.msgbox) { this.msgbox.removeAll(); }
+ };
+
+ /**
+ * If a message is currently showing, remove the specified part
+ */
+ LABKEY.DataRegion.prototype.removeMessage = function(part) {
+ if (this.msgbox) { this.msgbox.removeMessage(part); }
+ };
+
+ /**
+ * Show a message in the header of this DataRegion with a loading indicator.
+ * @param html the HTML source of the message to be shown
+ */
+ LABKEY.DataRegion.prototype.showLoadingMessage = function(html) {
+ html = html || "Loading...";
+ this.addMessage('
' + html + '
', 'drloading');
+ };
+
+ LABKEY.DataRegion.prototype.hideLoadingMessage = function() {
+ this.removeMessage('drloading');
+ };
+
+ /**
+ * Show a success message in the header of this DataRegion.
+ * @param html the HTML source of the message to be shown
+ */
+ LABKEY.DataRegion.prototype.showSuccessMessage = function(html) {
+ html = html || "Completed successfully.";
+ this.addMessage('
' + html + '
');
+ };
+
+ /**
+ * Show an error message in the header of this DataRegion.
+ * @param html the HTML source of the message to be shown
+ */
+ LABKEY.DataRegion.prototype.showErrorMessage = function(html) {
+ html = html || "An error occurred.";
+ this.addMessage('
' + html + '
');
+ };
+
+ LABKEY.DataRegion.prototype.showContext = function() {
+ _initContexts();
+
+ var contexts = [
+ _getContextBarSelector(this),
+ _getViewBarSelector(this)
+ ];
+
+ for (var i = 0; i < contexts.length; i++) {
+ var ctx = contexts[i];
+ var html = ctx.html();
+
+ if (html && html.trim() !== '') {
+ ctx.show();
+ }
+ }
+ };
+
+ /**
+ * Show a message in the header of this DataRegion.
+ * @param msg the HTML source of the message to be shown
+ * @deprecated use addMessage(msg, part) instead.
+ */
+ LABKEY.DataRegion.prototype.showMessage = function(msg) {
+ if (this.msgbox) {
+ this.msgbox.addMessage(msg);
+ }
+ };
+
+ LABKEY.DataRegion.prototype.showMessageArea = function() {
+ if (this.msgbox && this.msgbox.hasContent()) {
+ this.msgbox.show();
+ }
+ };
+
+ //
+ // Sections
+ //
+
+ LABKEY.DataRegion.prototype.displaySection = function(options) {
+ var dir = options && options.dir ? options.dir : 'n';
+
+ var sec = _getSectionSelector(this, dir);
+ if (options && options.html) {
+ options.append === true ? sec.append(options.html) : sec.html(options.html);
+ }
+ sec.show();
+ };
+
+ LABKEY.DataRegion.prototype.hideSection = function(options) {
+ var dir = options && options.dir ? options.dir : 'n';
+ var sec = _getSectionSelector(this, dir);
+
+ sec.hide();
+
+ if (options && options.clear === true) {
+ sec.html('');
+ }
+ };
+
+ LABKEY.DataRegion.prototype.writeSection = function(content, options) {
+ var append = options && options.append === true;
+ var dir = options && options.dir ? options.dir : 'n';
+
+ var sec = _getSectionSelector(this, dir);
+ append ? sec.append(content) : sec.html(content);
+ };
+
+ //
+ // Sorting
+ //
+
+ /**
+ * Replaces the sort on the given column, if present, or sets a brand new sort
+ * @param {string or LABKEY.FieldKey} fieldKey name of the column to be sorted
+ * @param {string} [sortDir=+] Set to '+' for ascending or '-' for descending
+ */
+ LABKEY.DataRegion.prototype.changeSort = function(fieldKey, sortDir) {
+ if (!fieldKey)
+ return;
+
+ fieldKey = _resolveFieldKey(this, fieldKey);
+
+ var columnName = fieldKey.toString();
+
+ var event = $.Event("beforesortchange");
+
+ $(this).trigger(event, [this, columnName, sortDir]);
+
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+
+ // no need to re-query for totalRowCount, if async
+ this.skipTotalRowCount = true;
+
+ this._userSort = _alterSortString(this, this._userSort, fieldKey, sortDir);
+ _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]);
+ };
+
+ /**
+ * Removes the sort on a specified column
+ * @param {string or LABKEY.FieldKey} fieldKey name of the column
+ */
+ LABKEY.DataRegion.prototype.clearSort = function(fieldKey) {
+ if (!fieldKey)
+ return;
+
+ fieldKey = _resolveFieldKey(this, fieldKey);
+
+ var columnName = fieldKey.toString();
+
+ var event = $.Event("beforeclearsort");
+
+ $(this).trigger(event, [this, columnName]);
+
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+
+ // no need to re-query for totalRowCount, if async
+ this.skipTotalRowCount = true;
+
+ this._userSort = _alterSortString(this, this._userSort, fieldKey);
+ if (this._userSort.length > 0) {
+ _setParameter(this, SORT_PREFIX, this._userSort, [SORT_PREFIX, OFFSET_PREFIX]);
+ }
+ else {
+ _removeParameters(this, [SORT_PREFIX, OFFSET_PREFIX]);
+ }
+ };
+
+ /**
+ * Returns the user sort from the URL. The sort is represented as an Array of objects of the form:
+ *
+ *
fieldKey: {String} The field key of the sort.
+ *
dir: {String} The sort direction, either "+" or "-".
+ *
+ * @returns {Object} Object representing the user sort.
+ */
+ LABKEY.DataRegion.prototype.getUserSort = function() {
+ return _getUserSort(this);
+ };
+
+ //
+ // Paging
+ //
+
+ var _initPaging = function() {
+ if (this.showPagination) {
+ // Issue 51036: load totalRows count async for DataRegions
+ if (!this.complete && this.showPaginationCountAsync && !this.skipTotalRowCount && this.loadingTotalRows === undefined) {
+ var params = _getAsyncParams(this, _getParameters(this), false);
+ var jsonData = _getAsyncBody(this, params);
+ _loadAsyncTotalRowCount(this, params, jsonData);
+ }
+
+ var ct = _getBarSelector(this).find('.labkey-pagination');
+
+ if (ct && ct.length) {
+ var hasOffset = $.isNumeric(this.offset);
+ var hasTotal = $.isNumeric(this.totalRows);
+
+ // display the counts
+ if (hasOffset) {
+
+ // small result set
+ if (hasTotal && this.totalRows < 5) {
+ return;
+ }
+
+ var low = this.offset + 1;
+ var high = this.offset + this.rowCount;
+
+ // user has opted to show all rows
+ if (hasTotal && (this.rowCount === null || this.rowCount < 1)) {
+ high = this.totalRows;
+ }
+
+ var showFirst = _showFirstEnabled(this);
+ var showLast = _showLastEnabled(this);
+ var showAll = _showAllEnabled(this);
+ this.showFirstID = LABKEY.Utils.id();
+ this.showLastID = LABKEY.Utils.id();
+ this.showAllID = LABKEY.Utils.id();
+
+ // If modifying this ensure it is consistent with DOM generated by PopupMenu.java
+ var elems = [
+ '
'
+ ]);
+ }
+
+ if (html.length) {
+ ctxBar.append(html.join(''));
+ ctxBar.find('.ctx-clear-var').off('click').on('click', $.proxy(this.clearAllParameters, this));
+ ctxBar.find('.ctx-clear-all').off('click').on('click', $.proxy(this.clearAllFilters, this));
+ }
+
+ // Issue 35396: Support ButtonBarOptions
+ if (LABKEY.Utils.isArray(this.buttonBarOnRenders)) {
+ for (var i=0; i < this.buttonBarOnRenders.length; i++) {
+ var scriptFnName = this.buttonBarOnRenders[i];
+ var fnParts = scriptFnName.split('.');
+ var scope = window;
+ var called = false;
+
+ for (var j=0; j < fnParts.length; j++) {
+ scope = scope[fnParts[j]];
+ if (!scope) break;
+ if (j === fnParts.length - 1 && LABKEY.Utils.isFunction(scope)) {
+ scope(this);
+ called = true;
+ }
+ }
+
+ if (!called) {
+ console.warn('Unable to call "' + scriptFnName + '" for DataRegion.ButtonBar.onRender.');
+ }
+ }
+ }
+ };
+
+ //
+ // Customize View
+ //
+ var _initCustomViews = function() {
+ if (this.view && this.view.session) {
+ // clear old contents
+ _getViewBarSelector(this).find('.labkey-button-bar').remove();
+
+ _getViewBarSelector(this).append([
+ '
',
+ 'This grid view has been modified.',
+ 'Revert',
+ 'Edit',
+ 'Save',
+ '
'
+ ].join(''));
+ _getViewBarSelector(this).find('.unsavedview-revert').off('click').on('click', $.proxy(function() {
+ _revertCustomView(this);
+ }, this));
+ _getViewBarSelector(this).find('.unsavedview-edit').off('click').on('click', $.proxy(function() {
+ this.showCustomizeView(undefined);
+ }, this));
+ _getViewBarSelector(this).find('.unsavedview-save').off('click').on('click', $.proxy(function() {
+ _saveSessionCustomView(this);
+ }, this));
+ }
+ };
+
+ /**
+ * Change the currently selected view to the named view
+ * @param {Object} view An object which contains the following properties.
+ * @param {String} [view.type] the type of view, either a 'view' or a 'report'.
+ * @param {String} [view.viewName] If the type is 'view', then the name of the view.
+ * @param {String} [view.reportId] If the type is 'report', then the report id.
+ * @param {Object} urlParameters NOTE: Experimental parameter; may change without warning. A set of filter and sorts to apply as URL parameters when changing the view.
+ */
+ LABKEY.DataRegion.prototype.changeView = function(view, urlParameters) {
+ var event = $.Event('beforechangeview');
+ $(this).trigger(event, [this, view, urlParameters]);
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+
+ var paramValPairs = [],
+ newSort = [],
+ skipPrefixes = [OFFSET_PREFIX, SHOW_ROWS_PREFIX, VIEWNAME_PREFIX, REPORTID_PREFIX];
+
+ // clear sibling parameters
+ this.viewName = undefined;
+ this.reportId = undefined;
+
+ if (view) {
+ if (LABKEY.Utils.isString(view)) {
+ paramValPairs.push([VIEWNAME_PREFIX, view]);
+ this.viewName = view;
+ }
+ else if (view.type === 'report') {
+ paramValPairs.push([REPORTID_PREFIX, view.reportId]);
+ this.reportId = view.reportId;
+ }
+ else if (view.type === 'view' && view.viewName) {
+ paramValPairs.push([VIEWNAME_PREFIX, view.viewName]);
+ this.viewName = view.viewName;
+ }
+ }
+
+ if (urlParameters) {
+ $.each(urlParameters.filter, function(i, filter) {
+ paramValPairs.push(['.' + filter.fieldKey + '~' + filter.op, filter.value]);
+ });
+
+ if (urlParameters.sort && urlParameters.sort.length > 0) {
+ $.each(urlParameters.sort, function(i, sort) {
+ newSort.push((sort.dir === '+' ? '' : sort.dir) + sort.fieldKey);
+ });
+ paramValPairs.push([SORT_PREFIX, newSort.join(',')]);
+ }
+
+ if (urlParameters.containerFilter) {
+ paramValPairs.push([CONTAINER_FILTER_NAME, urlParameters.containerFilter]);
+ }
+
+ // removes all filter, sort, and container filter parameters
+ skipPrefixes = skipPrefixes.concat([
+ ALL_FILTERS_SKIP_PREFIX, SORT_PREFIX, COLUMNS_PREFIX, CONTAINER_FILTER_NAME
+ ]);
+ }
+
+ // removes all filter, sort, and container filter parameters
+ _setParameters(this, paramValPairs, skipPrefixes);
+ };
+
+ LABKEY.DataRegion.prototype.getQueryDetails = function(success, failure, scope) {
+
+ var userSort = this.getUserSort(),
+ userColumns = this.getParameter(this.name + COLUMNS_PREFIX),
+ fields = [],
+ viewName = (this.view && this.view.name) || this.viewName || '';
+
+ var userFilter = this.getUserFilterArray().map(function(filter) {
+ var fieldKey = filter.getColumnName();
+ fields.push(fieldKey);
+
+ return {
+ fieldKey: fieldKey,
+ op: filter.getFilterType().getURLSuffix(),
+ value: filter.getValue()
+ };
+ });
+
+ userSort.forEach(function(sort) {
+ fields.push(sort.fieldKey);
+ });
+
+ LABKEY.Query.getQueryDetails({
+ containerPath: this.containerPath,
+ schemaName: this.schemaName,
+ queryName: this.queryName,
+ viewName: viewName,
+ fields: fields,
+ initializeMissingView: true,
+ success: function(queryDetails) {
+ success.call(scope || this, queryDetails, viewName, userColumns, userFilter, userSort);
+ },
+ failure: failure,
+ scope: scope
+ });
+ };
+
+ /**
+ * Hides the customize view interface if it is visible.
+ */
+ LABKEY.DataRegion.prototype.hideCustomizeView = function() {
+ if (this.activePanelId === CUSTOM_VIEW_PANELID) {
+ this.hideButtonPanel();
+ }
+ };
+
+ /**
+ * Show the customize view interface.
+ * @param activeTab {[String]} Optional. One of "ColumnsTab", "FilterTab", or "SortTab". If no value is specified (or undefined), the ColumnsTab will be shown.
+ */
+ LABKEY.DataRegion.prototype.showCustomizeView = function(activeTab) {
+ var region = this;
+
+ var panelConfig = this.getPanelConfiguration(CUSTOM_VIEW_PANELID);
+
+ if (!panelConfig) {
+
+ // whistle while we wait
+ var timerId = setTimeout(function() {
+ timerId = 0;
+ region.showLoadingMessage("Opening custom view designer...");
+ }, 500);
+
+ LABKEY.DataRegion.loadViewDesigner(function() {
+
+ var success = function(queryDetails, viewName, userColumns, userFilter, userSort) {
+ timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage();
+
+ // If there was an error parsing the query, we won't be able to render the customize view panel.
+ if (queryDetails.exception) {
+ var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', this.containerPath, {
+ schemaName: this.schemaName,
+ 'query.queryName': this.queryName
+ });
+ var msg = LABKEY.Utils.encodeHtml(queryDetails.exception) +
+ " View Source";
+
+ this.showErrorMessage(msg);
+ return;
+ }
+
+ this.customizeView = Ext4.create('LABKEY.internal.ViewDesigner.Designer', {
+ renderTo: Ext4.getBody().createChild({tag: 'div', customizeView: true, style: {display: 'none'}}),
+ activeTab: activeTab,
+ dataRegion: this,
+ containerPath : this.containerPath,
+ schemaName: this.schemaName,
+ queryName: this.queryName,
+ viewName: viewName,
+ query: queryDetails,
+ userFilter: userFilter,
+ userSort: userSort,
+ userColumns: userColumns,
+ userContainerFilter: this.getUserContainerFilter(),
+ allowableContainerFilters: this.allowableContainerFilters
+ });
+
+ this.customizeView.on('viewsave', function(designer, savedViewsInfo, urlParameters) {
+ _onViewSave.apply(this, [this, designer, savedViewsInfo, urlParameters]);
+ }, this);
+
+ this.customizeView.on({
+ beforedeleteview: function(cv, revert) {
+ _beforeViewDelete(region, revert);
+ },
+ deleteview: function(cv, success, json) {
+ _onViewDelete(region, success, json);
+ }
+ });
+
+ var first = true;
+
+ // Called when customize view needs to be shown
+ var showFn = function(id, panel, element, callback, scope) {
+ if (first) {
+ panel.hide();
+ panel.getEl().appendTo(Ext4.get(element[0]));
+ first = false;
+ }
+ panel.doLayout();
+ $(panel.getEl().dom).slideDown(undefined, function() {
+ panel.show();
+ callback.call(scope);
+ });
+ };
+
+ // Called when customize view needs to be hidden
+ var hideFn = function(id, panel, element, callback, scope) {
+ $(panel.getEl().dom).slideUp(undefined, function() {
+ panel.hide();
+ callback.call(scope);
+ });
+ };
+
+ this.publishPanel(CUSTOM_VIEW_PANELID, this.customizeView, showFn, hideFn, this);
+ this.showPanel(CUSTOM_VIEW_PANELID);
+ };
+ var failure = function() {
+ timerId > 0 ? clearTimeout(timerId) : this.hideLoadingMessage();
+ };
+
+ this.getQueryDetails(success, failure, this);
+ }, region);
+ }
+ else {
+ if (activeTab) {
+ panelConfig.panel.setActiveDesignerTab(activeTab);
+ }
+ this.showPanel(CUSTOM_VIEW_PANELID);
+ }
+ };
+
+ /**
+ * @ignore
+ * @private
+ * Shows/Hides customize view depending on if it is currently shown
+ */
+ LABKEY.DataRegion.prototype.toggleShowCustomizeView = function() {
+ if (this.activePanelId === CUSTOM_VIEW_PANELID) {
+ this.hideCustomizeView();
+ }
+ else {
+ this.showCustomizeView(undefined);
+ }
+ };
+
+ var _defaultShow = function(panelId, panel, ribbon, cb, cbScope) {
+ $('#' + panelId).slideDown(undefined, function() {
+ cb.call(cbScope);
+ });
+ };
+
+ var _defaultHide = function(panelId, panel, ribbon, cb, cbScope) {
+ $('#' + panelId).slideUp(undefined, function() {
+ cb.call(cbScope);
+ });
+ };
+
+ // TODO this is a pretty bad prototype, consider using config parameter with backward compat option
+ LABKEY.DataRegion.prototype.publishPanel = function(panelId, panel, showFn, hideFn, scope, friendlyName) {
+ this.panelConfigurations[panelId] = {
+ panelId: panelId,
+ panel: panel,
+ show: $.isFunction(showFn) ? showFn : _defaultShow,
+ hide: $.isFunction(hideFn) ? hideFn : _defaultHide,
+ scope: scope
+ };
+ if (friendlyName && friendlyName !== panelId)
+ this.panelConfigurations[friendlyName] = this.panelConfigurations[panelId];
+ return this;
+ };
+
+ LABKEY.DataRegion.prototype.getPanelConfiguration = function(panelId) {
+ return this.panelConfigurations[panelId];
+ };
+
+ /**
+ * @ignore
+ * Hides any panel that is currently visible. Returns a callback once the panel is hidden.
+ */
+ LABKEY.DataRegion.prototype.hidePanel = function(callback, scope) {
+ if (this.activePanelId) {
+ var config = this.getPanelConfiguration(this.activePanelId);
+ if (config) {
+
+ // find the ribbon container
+ var ribbon = _getDrawerSelector(this);
+
+ config.hide.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() {
+ this.activePanelId = undefined;
+ ribbon.hide();
+ if ($.isFunction(callback)) {
+ callback.call(scope || this);
+ }
+ LABKEY.Utils.signalWebDriverTest("dataRegionPanelHide");
+ $(this).trigger($.Event('afterpanelhide'), [this]);
+ }, this);
+ }
+ }
+ else {
+ if ($.isFunction(callback)) {
+ callback.call(scope || this);
+ }
+ }
+ };
+
+ LABKEY.DataRegion.prototype.showPanel = function(panelId, callback, scope) {
+
+ var config = this.getPanelConfiguration(panelId);
+
+ if (!config) {
+ console.error('Unable to find panel for id (' + panelId + '). Use publishPanel() to register a panel to be shown.');
+ return;
+ }
+
+ this.hideContext();
+ this.hideMessage(true);
+
+ this.hidePanel(function() {
+ this.activePanelId = config.panelId;
+
+ // ensure the ribbon is visible
+ var ribbon = _getDrawerSelector(this);
+ ribbon.show();
+
+ config.show.call(config.scope || this, this.activePanelId, config.panel, ribbon, function() {
+ if ($.isFunction(callback)) {
+ callback.call(scope || this);
+ }
+ LABKEY.Utils.signalWebDriverTest("dataRegionPanelShow");
+ $(this).trigger($.Event('afterpanelshow'), [this]);
+ }, this);
+ }, this);
+ };
+
+ function _hasPanelOpen(dr) {
+ return dr.activePanelId !== undefined;
+ }
+
+ function _hasButtonBarMenuOpen(dr) {
+ return _getBarSelector(dr).find(".lk-menu-drop.open").length > 0;
+ }
+
+ /**
+ * Returns true if the user has interacted with the DataRegion by changing
+ * the selection, opening a button menu, or opening a panel.
+ * @return {boolean}
+ * @private
+ */
+ LABKEY.DataRegion.prototype.isUserInteracting = function () {
+ return this.selectionModified || _hasPanelOpen(this) || _hasButtonBarMenuOpen(this);
+ };
+
+ //
+ // Misc
+ //
+
+ /**
+ * @private
+ */
+ var _initHeaderLocking = function() {
+ if (this._allowHeaderLock === true) {
+ this.hLock = new HeaderLock(this);
+ }
+ };
+
+ /**
+ * @private
+ */
+ var _initPanes = function() {
+ var callbacks = _paneCache[this.name];
+ if (callbacks) {
+ var me = this;
+ callbacks.forEach(function(config) {
+ config.cb.call(config.scope || me, me);
+ });
+ delete _paneCache[this.name];
+ }
+ };
+
+ /**
+ * @private
+ */
+ var _initReport = function() {
+ if (LABKEY.Utils.isObject(this.report)) {
+ this.addMessage({
+ html: [
+ 'Name:',
+ LABKEY.Utils.encodeHtml(this.report.name),
+ 'Source:',
+ LABKEY.Utils.encodeHtml(this.report.source)
+ ].join(' '),
+ part: 'report',
+ });
+ }
+ };
+
+ // These study specific functions/constants should be moved out of Data Region
+ // and into their own dependency.
+
+ var COHORT_LABEL = '/Cohort/Label';
+ var ADV_COHORT_LABEL = '/InitialCohort/Label';
+ var COHORT_ENROLLED = '/Cohort/Enrolled';
+ var ADV_COHORT_ENROLLED = '/InitialCohort/Enrolled';
+
+ /**
+ * DO NOT CALL DIRECTLY. This method is private and only available for removing cohort/group filters
+ * for this Data Region.
+ * @param subjectColumn
+ * @param groupNames
+ * @private
+ */
+ LABKEY.DataRegion.prototype._removeCohortGroupFilters = function(subjectColumn, groupNames) {
+ this.clearSelected({quiet: true});
+ var params = _getParameters(this);
+ var skips = [], i, p, k;
+
+ var keys = [
+ subjectColumn + COHORT_LABEL,
+ subjectColumn + ADV_COHORT_LABEL,
+ subjectColumn + COHORT_ENROLLED,
+ subjectColumn + ADV_COHORT_ENROLLED
+ ];
+
+ if (LABKEY.Utils.isArray(groupNames)) {
+ for (k=0; k < groupNames.length; k++) {
+ keys.push(subjectColumn + '/' + groupNames[k]);
+ }
+ }
+
+ for (i = 0; i < params.length; i++) {
+ p = params[i][0];
+ if (p.indexOf(this.name + '.') === 0) {
+ for (k=0; k < keys.length; k++) {
+ if (p.indexOf(keys[k] + '~') > -1) {
+ skips.push(p);
+ k = keys.length; // break loop
+ }
+ }
+ }
+ }
+
+ _updateFilter(this, undefined, skips);
+ };
+
+ /**
+ * DO NOT CALL DIRECTLY. This method is private and only available for replacing advanced cohort filters
+ * for this Data Region. Remove if advanced cohorts are removed.
+ * @param filter
+ * @private
+ */
+ LABKEY.DataRegion.prototype._replaceAdvCohortFilter = function(filter) {
+ this.clearSelected({quiet: true});
+ var params = _getParameters(this);
+ var skips = [], i, p;
+
+ for (i = 0; i < params.length; i++) {
+ p = params[i][0];
+ if (p.indexOf(this.name + '.') === 0) {
+ if (p.indexOf(COHORT_LABEL) > -1 || p.indexOf(ADV_COHORT_LABEL) > -1 || p.indexOf(COHORT_ENROLLED) > -1 || p.indexOf(ADV_COHORT_ENROLLED)) {
+ skips.push(p);
+ }
+ }
+ }
+
+ _updateFilter(this, filter, skips);
+ };
+
+ /**
+ * Looks for a column based on fieldKey, name, displayField, or caption (in that order)
+ * @param columnIdentifier
+ * @returns {*}
+ */
+ LABKEY.DataRegion.prototype.getColumn = function(columnIdentifier) {
+
+ var column = null, // backwards compat
+ isString = LABKEY.Utils.isString,
+ cols = this.columns;
+
+ if (isString(columnIdentifier) && LABKEY.Utils.isArray(cols)) {
+ $.each(['fieldKey', 'name', 'displayField', 'caption'], function(i, key) {
+ $.each(cols, function(c, col) {
+ if (isString(col[key]) && col[key] == columnIdentifier) {
+ column = col;
+ return false;
+ }
+ });
+ if (column) {
+ return false;
+ }
+ });
+ }
+
+ return column;
+ };
+
+ /**
+ * Returns a query config object suitable for passing into LABKEY.Query.selectRows() or other LABKEY.Query APIs.
+ * @returns {Object} Object representing the query configuration that generated this grid.
+ */
+ LABKEY.DataRegion.prototype.getQueryConfig = function() {
+ var config = {
+ dataRegionName: this.name,
+ dataRegionSelectionKey: this.selectionKey,
+ schemaName: this.schemaName,
+ viewName: this.viewName,
+ sort: this.getParameter(this.name + SORT_PREFIX),
+ // NOTE: The parameterized query values from QWP are included
+ parameters: this.getParameters(false),
+ containerFilter: this.containerFilter
+ };
+
+ if (this.queryName) {
+ config.queryName = this.queryName;
+ }
+ else if (this.sql) {
+ config.sql = this.sql;
+ }
+
+ var filters = this.getUserFilterArray();
+ if (filters.length > 0) {
+ config.filters = filters;
+ }
+
+ return config;
+ };
+
+ /**
+ * Hide the ribbon panel. If visible the ribbon panel will be hidden.
+ */
+ LABKEY.DataRegion.prototype.hideButtonPanel = function() {
+ this.hidePanel();
+ this.showContext();
+ this.showMessageArea();
+ };
+
+ /**
+ * Allows for asynchronous rendering of the Data Region. This region must be in "async" mode for
+ * this to do anything.
+ * @function
+ * @param {String} [renderTo] - The element ID where to render the data region. If not given it will default to
+ * the current renderTo target is.
+ */
+ LABKEY.DataRegion.prototype.render = function(renderTo) {
+ if (!this.RENDER_LOCK && this.async) {
+ _convertRenderTo(this, renderTo);
+ this.refresh();
+ }
+ };
+
+ /**
+ * Show a ribbon panel.
+ *
+ * first arg can be button on the button bar or target panel id/configuration
+ */
+
+ LABKEY.DataRegion.prototype.toggleButtonPanelHandler = function(panelButton) {
+ _toggleButtonPanel( this, $(panelButton).attr('data-labkey-panel-toggle'), null, true);
+ };
+
+ LABKEY.DataRegion.prototype.showButtonPanel = function(panel, optionalTab) {
+ _toggleButtonPanel(this, panel, optionalTab, false);
+ };
+
+ LABKEY.DataRegion.prototype.toggleButtonPanel = function(panel, optionalTab) {
+ _toggleButtonPanel(this, panel, optionalTab, true);
+ };
+
+ var _toggleButtonPanel = function(dr, panel, optionalTab, toggle) {
+ var ribbon = _getDrawerSelector(dr);
+ // first check if this is a named panel instead of a button element
+ var panelId, panelSel;
+ if (typeof panel === 'string' && dr.getPanelConfiguration(panel))
+ panelId = dr.getPanelConfiguration(panel).panelId;
+ else
+ panelId = panel;
+
+ if (panelId) {
+
+ panelSel = $('#' + panelId);
+
+ // allow for toggling the state
+ if (panelId === dr.activePanelId) {
+ if (toggle) {
+ dr.hideButtonPanel();
+ return;
+ }
+ }
+ else {
+ // determine if the content needs to be moved to the ribbon
+ if (ribbon.has(panelSel).length === 0) {
+ panelSel.detach().appendTo(ribbon);
+ }
+
+ // determine if this panel has been registered
+ if (!dr.getPanelConfiguration(panelId) && panelSel.length > 0) {
+ dr.publishPanel(panelId, panelId);
+ }
+
+ dr.showPanel(panelId);
+ }
+ if (optionalTab)
+ {
+ var t = panelSel.find('a[data-toggle="tab"][href="#' + optionalTab + '"]');
+ if (!t.length)
+ t = panelSel.find('a[data-toggle="tab"][data-tabName="' + optionalTab + '"]');
+ t.tab('show');
+ }
+ }
+ };
+
+ LABKEY.DataRegion.prototype.loadFaceting = function(cb, scope) {
+
+ var region = this;
+
+ var onLoad = function() {
+ region.facetLoaded = true;
+ if ($.isFunction(cb)) {
+ cb.call(scope || this);
+ }
+ };
+
+ LABKEY.requiresExt4ClientAPI(function() {
+ if (LABKEY.devMode) {
+ // should match study/ParticipantFilter.lib.xml
+ LABKEY.requiresScript([
+ '/study/ReportFilterPanel.js',
+ '/study/ParticipantFilterPanel.js'
+ ], function() {
+ LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad);
+ });
+ }
+ else {
+ LABKEY.requiresScript('/study/ParticipantFilter.min.js', function() {
+ LABKEY.requiresScript('/dataregion/panel/Facet.js', onLoad);
+ });
+ }
+ }, this);
+ };
+
+ LABKEY.DataRegion.prototype.showFaceting = function() {
+ if (this.facetLoaded) {
+ if (!this.facet) {
+ this.facet = LABKEY.dataregion.panel.Facet.display(this);
+ }
+ this.facet.toggleCollapse();
+ }
+ else {
+ this.loadFaceting(this.showFaceting, this);
+ }
+ };
+
+ LABKEY.DataRegion.prototype.on = function(evt, callback, scope) {
+ // Prevent from handing back the jQuery event itself.
+ $(this).bind(evt, function() { callback.apply(scope || this, $(arguments).slice(1)); });
+ };
+
+ LABKEY.DataRegion.prototype.one = function(evt, callback, scope) {
+ $(this).one(evt, function() { callback.apply(scope || this, $(arguments).slice(1)); });
+ };
+
+ LABKEY.DataRegion.prototype._onButtonClick = function(buttonId) {
+ var item = this.findButtonById(this.buttonBar.items, buttonId);
+ if (item && $.isFunction(item.handler)) {
+ try {
+ return item.handler.call(item.scope || this, this);
+ }
+ catch(ignore) {}
+ }
+ return false;
+ };
+
+ LABKEY.DataRegion.prototype.findButtonById = function(items, id) {
+ if (!items || !items.length || items.length <= 0) {
+ return null;
+ }
+
+ var ret;
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].id == id) {
+ return items[i];
+ }
+ ret = this.findButtonById(items[i].items, id);
+ if (null != ret) {
+ return ret;
+ }
+ }
+
+ return null;
+ };
+
+ LABKEY.DataRegion.prototype.headerLock = function() { return this._allowHeaderLock === true; };
+
+ LABKEY.DataRegion.prototype.disableHeaderLock = function() {
+ if (this.headerLock() && this.hLock) {
+ this.hLock.disable();
+ this.hLock = undefined;
+ }
+ };
+
+ /**
+ * Add or remove a summary statistic for a given column in the DataRegion query view.
+ * @param viewName
+ * @param colFieldKey
+ * @param summaryStatName
+ */
+ LABKEY.DataRegion.prototype.toggleSummaryStatForCustomView = function(viewName, colFieldKey, summaryStatName) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ var colProviderNames = [];
+ $.each(view.analyticsProviders, function(index, existingProvider) {
+ if (existingProvider.fieldKey === colFieldKey)
+ colProviderNames.push(existingProvider.name);
+ });
+
+ if (colProviderNames.indexOf(summaryStatName) === -1) {
+ _addAnalyticsProviderToView.call(this, view, colFieldKey, summaryStatName, true);
+ }
+ else {
+ _removeAnalyticsProviderFromView.call(this, view, colFieldKey, summaryStatName, true);
+ }
+ }
+ }, null, this);
+ };
+
+ /**
+ * Get the array of selected ColumnAnalyticsProviders for the given column FieldKey in a view.
+ * @param viewName
+ * @param colFieldKey
+ * @param callback
+ * @param callbackScope
+ */
+ LABKEY.DataRegion.prototype.getColumnAnalyticsProviders = function(viewName, colFieldKey, callback, callbackScope) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ var colProviderNames = [];
+ $.each(view.analyticsProviders, function(index, existingProvider) {
+ if (existingProvider.fieldKey === colFieldKey) {
+ colProviderNames.push(existingProvider.name);
+ }
+ });
+
+ if ($.isFunction(callback)) {
+ callback.call(callbackScope, colProviderNames);
+ }
+ }
+ }, null, this);
+ };
+
+ /**
+ * Set the summary statistic ColumnAnalyticsProviders for the given column FieldKey in the view.
+ * @param viewName
+ * @param colFieldKey
+ * @param summaryStatProviderNames
+ */
+ LABKEY.DataRegion.prototype.setColumnSummaryStatistics = function(viewName, colFieldKey, summaryStatProviderNames) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ var newAnalyticsProviders = [];
+ $.each(view.analyticsProviders, function(index, existingProvider) {
+ if (existingProvider.fieldKey !== colFieldKey || existingProvider.name.indexOf('AGG_') != 0) {
+ newAnalyticsProviders.push(existingProvider);
+ }
+ });
+
+ $.each(summaryStatProviderNames, function(index, providerName) {
+ newAnalyticsProviders.push({
+ fieldKey: colFieldKey,
+ name: providerName,
+ isSummaryStatistic: true
+ });
+ });
+
+ view.analyticsProviders = newAnalyticsProviders;
+ _updateSessionCustomView.call(this, view, true);
+ }
+ }, null, this);
+ };
+
+ /**
+ * Used via SummaryStatisticsAnalyticsProvider to show a dialog of the applicable summary statistics for a column in the view.
+ * @param colFieldKey
+ */
+ LABKEY.DataRegion.prototype.showColumnStatisticsDialog = function(colFieldKey) {
+ LABKEY.requiresScript('query/ColumnSummaryStatistics', function() {
+ var regionViewName = this.viewName || "",
+ column = this.getColumn(colFieldKey);
+
+ if (column) {
+ this.getColumnAnalyticsProviders(regionViewName, colFieldKey, function(colSummaryStats) {
+ Ext4.create('LABKEY.ext4.ColumnSummaryStatisticsDialog', {
+ queryConfig: this.getQueryConfig(),
+ filterArray: LABKEY.Filter.getFiltersFromUrl(this.selectAllURL, 'query'), //Issue 26594
+ containerPath: this.containerPath,
+ column: column,
+ initSelection: colSummaryStats,
+ listeners: {
+ scope: this,
+ applySelection: function(win, colSummaryStatsNames) {
+ win.getEl().mask("Applying selection...");
+ this.setColumnSummaryStatistics(regionViewName, colFieldKey, colSummaryStatsNames);
+ win.close();
+ }
+ }
+ }).show();
+ }, this);
+ }
+ }, this);
+ };
+
+ /**
+ * Remove a column from the given DataRegion query view.
+ * @param viewName
+ * @param colFieldKey
+ */
+ LABKEY.DataRegion.prototype.removeColumn = function(viewName, colFieldKey) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ var colFieldKeys = $.map(view.columns, function (c) {
+ return c.fieldKey;
+ }),
+ fieldKeyIndex = colFieldKeys.indexOf(colFieldKey);
+
+ if (fieldKeyIndex > -1) {
+ view.columns.splice(fieldKeyIndex, 1);
+ _updateSessionCustomView.call(this, view, true);
+ }
+ }
+ }, null, this);
+ };
+
+ /**
+ * Add the enabled analytics provider to the custom view definition based on the column fieldKey and provider name.
+ * In addition, disable the column menu item if the column is visible in the grid.
+ * @param viewName
+ * @param colFieldKey
+ * @param providerName
+ */
+ LABKEY.DataRegion.prototype.addAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ _addAnalyticsProviderToView.call(this, view, colFieldKey, providerName, false);
+ _updateAnalyticsProviderMenuItem(this.name + ':' + colFieldKey, providerName, true);
+ }
+ }, null, this);
+ };
+
+ /**
+ * Remove an enabled analytics provider from the custom view definition based on the column fieldKey and provider name.
+ * In addition, enable the column menu item if the column is visible in the grid.
+ * @param viewName
+ * @param colFieldKey
+ * @param providerName
+ */
+ LABKEY.DataRegion.prototype.removeAnalyticsProviderForCustomView = function(viewName, colFieldKey, providerName) {
+ this.getQueryDetails(function(queryDetails) {
+ var view = _getViewFromQueryDetails(queryDetails, viewName);
+ if (view && _viewContainsColumn(view, colFieldKey)) {
+ _removeAnalyticsProviderFromView.call(this, view, colFieldKey, providerName, false);
+ _updateAnalyticsProviderMenuItem(this.name + ':' + colFieldKey, providerName, false);
+ }
+ }, null, this);
+ };
+
+ /**
+ * @private
+ */
+ LABKEY.DataRegion.prototype._openFilter = function(columnName, evt) {
+ if (evt && $(evt.target).hasClass('fa-close')) {
+ return;
+ }
+
+ var column = this.getColumn(columnName);
+
+ if (column) {
+ var show = function() {
+ this._dialogLoaded = true;
+ new LABKEY.FilterDialog({
+ dataRegionName: this.name,
+ column: this.getColumn(columnName),
+ cacheFacetResults: false // could have changed on Ajax
+ }).show();
+ }.bind(this);
+
+ this._dialogLoaded ? show() : LABKEY.requiresExt3ClientAPI(show);
+ }
+ else {
+ LABKEY.Utils.alert('Column not available', 'Unable to find column "' + columnName + '" in this view.');
+ }
+ };
+
+ var _updateSessionCustomView = function(customView, requiresRefresh) {
+ var viewConfig = $.extend({}, customView, {
+ shared: false,
+ inherit: false,
+ hidden: false,
+ session: true
+ });
+
+ LABKEY.Query.saveQueryViews({
+ containerPath: this.containerPath,
+ schemaName: this.schemaName,
+ queryName: this.queryName,
+ views: [viewConfig],
+ scope: this,
+ success: function(info) {
+ if (requiresRefresh) {
+ this.refresh();
+ }
+ else if (info.views.length === 1) {
+ this.view = info.views[0];
+ _initCustomViews.call(this);
+ this.showContext();
+ }
+ }
+ });
+ };
+
+ var _addAnalyticsProviderToView = function(view, colFieldKey, providerName, isSummaryStatistic) {
+ var colProviderNames = [];
+ $.each(view.analyticsProviders, function(index, existingProvider) {
+ if (existingProvider.fieldKey === colFieldKey)
+ colProviderNames.push(existingProvider.name);
+ });
+
+ if (colProviderNames.indexOf(providerName) === -1) {
+ view.analyticsProviders.push({
+ fieldKey: colFieldKey,
+ name: providerName,
+ isSummaryStatistic: isSummaryStatistic
+ });
+
+ _updateSessionCustomView.call(this, view, isSummaryStatistic);
+ }
+ };
+
+ var _removeAnalyticsProviderFromView = function(view, colFieldKey, providerName, isSummaryStatistic) {
+ var indexToRemove = null;
+ $.each(view.analyticsProviders, function(index, existingProvider) {
+ if (existingProvider.fieldKey === colFieldKey && existingProvider.name === providerName) {
+ indexToRemove = index;
+ return false;
+ }
+ });
+
+ if (indexToRemove != null) {
+ view.analyticsProviders.splice(indexToRemove, 1);
+ _updateSessionCustomView.call(this, view, isSummaryStatistic);
+ }
+ };
+
+ /**
+ * Attempt to find a DataRegion analytics provider column menu item so that it can be either enabled to allow
+ * it to once again be selected after removal or disabled so that it can't be selected a second time.
+ * @param columnName the DataRegion column th element column-name attribute
+ * @param providerName the analytics provider name
+ * @param disable
+ * @private
+ */
+ var _updateAnalyticsProviderMenuItem = function(columnName, providerName, disable) {
+ var menuItemEl = $("th[column-name|='" + columnName + "']").find("a[onclick*='" + providerName + "']").parent();
+ if (menuItemEl) {
+ if (disable) {
+ menuItemEl.addClass('disabled');
+ }
+ else {
+ menuItemEl.removeClass('disabled');
+ }
+ }
+ };
+
+ //
+ // PRIVATE FUNCTIONS
+ //
+ var _applyOptionalParameters = function(region, params, optionalParams) {
+ optionalParams.forEach(function(p) {
+ if (LABKEY.Utils.isObject(p)) {
+ if (region[p.name] !== undefined) {
+ if (p.check && !p.check.call(region, region[p.name])) {
+ return;
+ }
+ if (p.prefix) {
+ params[region.name + '.' + p.name] = region[p.name];
+ }
+ else {
+ params[p.name] = region[p.name];
+ }
+ }
+ }
+ else if (p && region[p] !== undefined) {
+ params[p] = region[p];
+ }
+ });
+ };
+
+ var _alterSortString = function(region, current, fieldKey, direction /* optional */) {
+ fieldKey = _resolveFieldKey(region, fieldKey);
+
+ var columnName = fieldKey.toString(),
+ newSorts = [];
+
+ if (current != null) {
+ current.split(',').forEach(function(sort) {
+ if (sort.length > 0 && (sort != columnName) && (sort != SORT_ASC + columnName) && (sort != SORT_DESC + columnName)) {
+ newSorts.push(sort);
+ }
+ });
+ }
+
+ if (direction === SORT_ASC) { // Easier to read without the encoded + on the URL...
+ direction = '';
+ }
+
+ if (LABKEY.Utils.isString(direction)) {
+ newSorts = [direction + columnName].concat(newSorts);
+ }
+
+ return newSorts.join(',');
+ };
+
+ var _ensureFilterDateFormat = function(value) {
+ if (LABKEY.Utils.isDate(value)) {
+ value = $.format.date(value, 'yyyy-MM-dd');
+ if (LABKEY.Utils.endsWith(value, 'Z')) {
+ value = value.substring(0, value.length - 1);
+ }
+ }
+
+ return value;
+ }
+
+ var _buildQueryString = function(region, pairs) {
+ if (!LABKEY.Utils.isArray(pairs)) {
+ return '';
+ }
+
+ var queryParts = [], key, value;
+
+ pairs.forEach(function(pair) {
+ key = pair[0];
+ value = pair.length > 1 ? pair[1] : undefined;
+
+ queryParts.push(encodeURIComponent(key));
+ if (LABKEY.Utils.isDefined(value)) {
+
+ value = _ensureFilterDateFormat(value);
+ queryParts.push('=');
+ queryParts.push(encodeURIComponent(value));
+ }
+ queryParts.push('&');
+ });
+
+ if (queryParts.length > 0) {
+ queryParts.pop();
+ }
+
+ return queryParts.join("");
+ };
+
+ var _chainSelectionCountCallback = function(region, config) {
+
+ var success = LABKEY.Utils.getOnSuccess(config);
+
+ // On success, update the current selectedCount on this DataRegion and fire the 'selectchange' event
+ config.success = function(data) {
+ region.removeMessage('selection');
+ region.selectionModified = true;
+ region.selectedCount = data.count;
+ _onSelectionChange(region);
+
+ // Chain updateSelected with the user-provided success callback
+ if ($.isFunction(success)) {
+ success.call(config.scope, data);
+ }
+ };
+
+ return config;
+ };
+
+ var _convertRenderTo = function(region, renderTo) {
+ if (renderTo) {
+ if (LABKEY.Utils.isString(renderTo)) {
+ region.renderTo = renderTo;
+ }
+ else if (LABKEY.Utils.isString(renderTo.id)) {
+ region.renderTo = renderTo.id; // support 'Ext' elements
+ }
+ else {
+ throw 'Unsupported "renderTo"';
+ }
+ }
+
+ return region;
+ };
+
+ var _deleteTimer;
+
+ var _beforeViewDelete = function(region, revert) {
+ _deleteTimer = setTimeout(function() {
+ _deleteTimer = 0;
+ region.showLoadingMessage(revert ? 'Reverting view...' : 'Deleting view...');
+ }, 500);
+ };
+
+ var _onViewDelete = function(region, success, json) {
+ if (_deleteTimer) {
+ clearTimeout(_deleteTimer);
+ }
+
+ if (success) {
+ region.removeMessage.call(region, 'customizeview');
+ region.showSuccessMessage.call(region);
+
+ // change view to either a shadowed view or the default view
+ var config = { type: 'view' };
+ if (json.viewName) {
+ config.viewName = json.viewName;
+ }
+ region.changeView.call(region, config);
+ }
+ else {
+ region.removeMessage.call(region, 'customizeview');
+ region.showErrorMessage.call(region, json.exception);
+ }
+ };
+
+ // The view can be reverted without ViewDesigner present
+ var _revertCustomView = function(region) {
+ _beforeViewDelete(region, true);
+
+ var config = {
+ schemaName: region.schemaName,
+ queryName: region.queryName,
+ containerPath: region.containerPath,
+ revert: true,
+ success: function(json) {
+ _onViewDelete(region, true /* success */, json);
+ },
+ failure: function(json) {
+ _onViewDelete(region, false /* success */, json);
+ }
+ };
+
+ if (region.viewName) {
+ config.viewName = region.viewName;
+ }
+
+ LABKEY.Query.deleteQueryView(config);
+ };
+
+ var _getViewFromQueryDetails = function(queryDetails, viewName) {
+ var matchingView;
+
+ $.each(queryDetails.views, function(index, view) {
+ if (view.name === viewName) {
+ matchingView = view;
+ return false;
+ }
+ });
+
+ return matchingView;
+ };
+
+ var _viewContainsColumn = function(view, colFieldKey) {
+ var keys = $.map(view.columns, function(c) {
+ return c.fieldKey.toLowerCase();
+ });
+ var exists = colFieldKey && keys.indexOf(colFieldKey.toLowerCase()) > -1;
+
+ if (!exists) {
+ console.warn('Unable to find column in view: ' + colFieldKey);
+ }
+
+ return exists;
+ };
+
+ var _getAllRowSelectors = function(region) {
+ return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".toggle"]');
+ };
+
+ var _getBarSelector = function(region) {
+ return $('#' + region.domId + '-headerbar');
+ };
+
+ var _getContextBarSelector = function(region) {
+ return $('#' + region.domId + '-ctxbar');
+ };
+
+ var _getDrawerSelector = function(region) {
+ return $('#' + region.domId + '-drawer');
+ };
+
+ var _getFormSelector = function(region) {
+ var form = $('form#' + region.domId + '-form');
+
+ // derived DataRegion's may not include the form id
+ if (form.length === 0) {
+ form = $('#' + region.domId).closest('form');
+ }
+
+ return form;
+ };
+
+ var _getHeaderSelector = function(region) {
+ return $('#' + region.domId + '-header');
+ };
+
+ var _getRowSelectors = function(region) {
+ return _getFormSelector(region).find('.labkey-selectors input[type="checkbox"][name=".select"]');
+ };
+
+ var _getSectionSelector = function(region, dir) {
+ return $('#' + region.domId + '-section-' + dir);
+ };
+
+ var _getShowFirstSelector = function(region) {
+ return $('#' + region.showFirstID);
+ };
+
+ var _getShowLastSelector = function(region) {
+ return $('#' + region.showLastID);
+ };
+
+ var _getShowAllSelector = function(region) {
+ return $('#' + region.showAllID);
+ };
+
+ // Formerly, LABKEY.DataRegion.getParamValPairsFromString / LABKEY.DataRegion.getParamValPairs
+ var _getParameters = function(region, skipPrefixSet /* optional */) {
+
+ var params = [];
+ var qString = region.requestURL;
+
+ if (LABKEY.Utils.isString(qString) && qString.length > 0) {
+
+ var qmIdx = qString.indexOf('?');
+ if (qmIdx > -1) {
+ qString = qString.substring(qmIdx + 1);
+
+ var poundIdx = qString.indexOf('#');
+ if (poundIdx > -1)
+ qString = qString.substr(0, poundIdx);
+
+ if (qString.length > 1) {
+ var pairs = qString.split('&'), p, key,
+ LAST = '.lastFilter', lastIdx, skip = LABKEY.Utils.isArray(skipPrefixSet);
+
+ var exactMatches = EXACT_MATCH_PREFIXES.map(function (prefix) {
+ return region.name + prefix;
+ });
+
+ $.each(pairs, function (i, pair) {
+ p = pair.split('=', 2);
+ key = p[0] = decodeURIComponent(p[0]);
+ lastIdx = key.indexOf(LAST);
+
+ if (lastIdx > -1 && lastIdx === (key.length - LAST.length)) {
+ return;
+ }
+ else if (REQUIRE_NAME_PREFIX.hasOwnProperty(key)) {
+ // Issue 26686: Block known parameters, should be prefixed by region name
+ return;
+ }
+
+ var stop = false;
+ if (skip) {
+ $.each(skipPrefixSet, function (j, skipPrefix) {
+ if (LABKEY.Utils.isString(skipPrefix)) {
+
+ // Special prefix that should remove all filters, but no other parameters for the current grid
+ if (skipPrefix.indexOf(ALL_FILTERS_SKIP_PREFIX) === (skipPrefix.length - 2)) {
+ if (key.indexOf(region.name + '.') === 0 && key.indexOf('~') > 0) {
+
+ stop = true;
+ return false;
+ }
+ }
+ else {
+ if (exactMatches.indexOf(skipPrefix) > -1) {
+ if (key === skipPrefix) {
+ stop = true;
+ return false;
+ }
+ }
+ else if (key.toLowerCase().indexOf(skipPrefix.toLowerCase()) === 0) {
+ // only skip filters, parameters, and sorts for the current grid
+ if (key.indexOf(region.name + '.') === 0 &&
+
+ (key === skipPrefix ||
+ key.indexOf('~') > 0 ||
+ key.indexOf(PARAM_PREFIX) > 0 ||
+ key === (skipPrefix + 'sort'))) {
+ stop = true;
+ return false;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ if (!stop) {
+ if (p.length > 1) {
+ p[1] = decodeURIComponent(p[1]);
+ }
+ params.push(p);
+ }
+ });
+ }
+ }
+ }
+
+ return params;
+ };
+
+ /**
+ *
+ * @param region
+ * @param {boolean} [asString=false]
+ * @private
+ */
+ var _getUserSort = function(region, asString) {
+ var userSort = [],
+ sortParam = region.getParameter(region.name + SORT_PREFIX);
+
+ if (asString) {
+ userSort = sortParam || '';
+ }
+ else {
+ if (sortParam) {
+ var fieldKey, dir;
+ sortParam.split(',').forEach(function(sort) {
+ fieldKey = sort;
+ dir = SORT_ASC;
+ if (sort.charAt(0) === SORT_DESC) {
+ fieldKey = fieldKey.substring(1);
+ dir = SORT_DESC;
+ }
+ else if (sort.charAt(0) === SORT_ASC) {
+ fieldKey = fieldKey.substring(1);
+ }
+ userSort.push({fieldKey: fieldKey, dir: dir});
+ });
+ }
+ }
+
+ return userSort;
+ };
+
+ var _getViewBarSelector = function(region) {
+ return $('#' + region.domId + '-viewbar');
+ };
+
+ 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);
+ }, region));
+ };
+
+ var _onRenderMessageArea = function(region, parts) {
+ var msgArea = region.msgbox;
+ if (msgArea) {
+ if (region.showRecordSelectors && parts['selection']) {
+ _buttonSelectionBind(region, '.select-all', region.selectAll);
+ _buttonSelectionBind(region, '.select-none', region.clearSelected);
+ _buttonSelectionBind(region, '.show-all', region.showAll);
+ _buttonSelectionBind(region, '.show-selected', region.showSelectedRows);
+ _buttonSelectionBind(region, '.show-unselected', region.showUnselectedRows);
+ }
+ else if (parts['customizeview']) {
+ _buttonSelectionBind(region, '.unsavedview-revert', function() { _revertCustomView(this); });
+ _buttonSelectionBind(region, '.unsavedview-edit', function() { this.showCustomizeView(undefined); });
+ _buttonSelectionBind(region, '.unsavedview-save', function() { _saveSessionCustomView(this); });
+ }
+ }
+ };
+
+ var _onSelectionChange = function(region) {
+ $(region).trigger('selectchange', [region, region.selectedCount]);
+ _updateRequiresSelectionButtons(region, region.selectedCount);
+ LABKEY.Utils.signalWebDriverTest('dataRegionUpdate', region.selectedCount);
+ LABKEY.Utils.signalWebDriverTest('dataRegionUpdate-' + region.name, region.selectedCount);
+ };
+
+ var _onViewSave = function(region, designer, savedViewsInfo, urlParameters) {
+ if (savedViewsInfo && savedViewsInfo.views.length > 0) {
+ region.hideCustomizeView.call(region);
+ region.changeView.call(region, {
+ type: 'view',
+ viewName: savedViewsInfo.views[0].name
+ }, urlParameters);
+ }
+ };
+
+ var _removeParameters = function(region, skipPrefixes /* optional */) {
+ return _setParameters(region, null, skipPrefixes);
+ };
+
+ var _resolveFieldKey = function(region, fieldKey) {
+ var fk = fieldKey;
+ if (!(fk instanceof LABKEY.FieldKey)) {
+ fk = LABKEY.FieldKey.fromString('' + fk);
+ }
+ return fk;
+ };
+
+ var _saveSessionCustomView = function(region) {
+ // Note: currently only will save session views. Future version could create a new view using url sort/filters.
+ if (!(region.view && region.view.session)) {
+ return;
+ }
+
+ // Get the canEditSharedViews permission and candidate targetContainers.
+ var viewName = (region.view && region.view.name) || region.viewName || '';
+
+ LABKEY.Query.getQueryDetails({
+ schemaName: region.schemaName,
+ queryName: region.queryName,
+ viewName: viewName,
+ initializeMissingView: false,
+ containerPath: region.containerPath,
+ success: function (json) {
+ // Display an error if there was an issue error getting the query details
+ if (json.exception) {
+ var viewSourceUrl = LABKEY.ActionURL.buildURL('query', 'viewQuerySource.view', null, {schemaName: this.schemaName, "query.queryName": this.queryName});
+ var msg = LABKEY.Utils.encodeHtml(json.exception) + " View Source";
+
+ this.showErrorMessage.call(this, msg);
+ return;
+ }
+
+ _saveSessionShowPrompt(this, json);
+ },
+ scope: region
+ });
+ };
+
+ var _saveSessionView = function(o, region, win) {
+ var timerId = setTimeout(function() {
+ timerId = 0;
+ Ext4.Msg.progress("Saving...", "Saving custom view...");
+ }, 500);
+
+ var jsonData = {
+ schemaName: region.schemaName,
+ "query.queryName": region.queryName,
+ "query.viewName": region.viewName,
+ newName: o.name,
+ inherit: o.inherit,
+ shared: o.shared,
+ hidden: o.hidden,
+ replace: o.replace,
+ };
+
+ if (o.inherit) {
+ jsonData.containerPath = o.containerPath;
+ }
+
+ LABKEY.Ajax.request({
+ url: LABKEY.ActionURL.buildURL('query', 'saveSessionView', region.containerPath),
+ method: 'POST',
+ jsonData: jsonData,
+ callback: function() {
+ if (timerId > 0)
+ clearTimeout(timerId);
+ win.close();
+ },
+ success: function() {
+ region.showSuccessMessage.call(region);
+ region.changeView.call(region, {type: 'view', viewName: o.name});
+ },
+ failure: function(resp) {
+ var json = resp.responseText ? Ext4.decode(resp.responseText) : resp;
+ if (json.exception && json.exception.indexOf('A saved view by the name') === 0) {
+
+ Ext4.Msg.show({
+ title : "Duplicate View Name",
+ msg : json.exception + " Would you like to replace it?",
+ cls : 'data-window',
+ icon : Ext4.Msg.QUESTION,
+ buttons : Ext4.Msg.YESNO,
+ fn : function(btn) {
+ if (btn === 'yes') {
+ o.replace = true;
+ _saveSessionView(o, region, win);
+ }
+ },
+ scope : this
+ });
+ }
+ else
+ Ext4.Msg.alert('Error saving view', json.exception || json.statusText || Ext4.decode(json.responseText).exception);
+ },
+ scope: region
+ });
+ };
+
+ var _saveSessionShowPrompt = function(region, queryDetails) {
+ LABKEY.DataRegion.loadViewDesigner(function() {
+ var config = Ext4.applyIf({
+ allowableContainerFilters: region.allowableContainerFilters,
+ targetContainers: queryDetails.targetContainers,
+ canEditSharedViews: queryDetails.canEditSharedViews,
+ canEdit: LABKEY.DataRegion.getCustomViewEditableErrors(config).length == 0,
+ success: function (win, o) {
+ _saveSessionView(o, region, win);
+ },
+ scope: region
+ }, region.view);
+
+ LABKEY.internal.ViewDesigner.Designer.saveCustomizeViewPrompt(config);
+ });
+ };
+
+ var _setParameter = function(region, param, value, skipPrefixes /* optional */) {
+ _setParameters(region, [[param, value]], skipPrefixes);
+ };
+
+ var _setParameters = function(region, newParamValPairs, skipPrefixes /* optional */) {
+ // prepend region name
+ // e.g. ['.hello', '.goodbye'] becomes ['aqwp19.hello', 'aqwp19.goodbye']
+ if (LABKEY.Utils.isArray(skipPrefixes)) {
+ skipPrefixes.forEach(function(skip, i) {
+ if (skip && skip.indexOf(region.name + '.') !== 0) {
+ skipPrefixes[i] = region.name + skip;
+ }
+ });
+ }
+
+ var param, value,
+ params = _getParameters(region, skipPrefixes);
+
+ if (LABKEY.Utils.isArray(newParamValPairs)) {
+ newParamValPairs.forEach(function(newPair) {
+ if (!LABKEY.Utils.isArray(newPair)) {
+ throw new Error("DataRegion: _setParameters newParamValPairs improperly initialized. It is an array of arrays. You most likely passed in an array of strings.");
+ }
+ param = newPair[0];
+ value = newPair[1];
+
+ // Allow value to be null/undefined to support no-value filter types (Is Blank, etc)
+ if (LABKEY.Utils.isString(param) && param.length > 1) {
+ if (param.indexOf(region.name) !== 0) {
+ param = region.name + param;
+ }
+
+ params.push([param, value]);
+ }
+ });
+ }
+
+ if (region.async) {
+ _load(region, params, skipPrefixes);
+ }
+ else {
+ region.setSearchString.call(region, region.name, _buildQueryString(region, params));
+ }
+ };
+
+ var _showRows = function(region, showRowsEnum) {
+ // no need to re-query for totalRowCount, if async
+ this.skipTotalRowCount = true;
+
+ // clear sibling parameters, could we do this with events?
+ this.maxRows = undefined;
+ this.offset = 0;
+
+ _setParameter(region, SHOW_ROWS_PREFIX, showRowsEnum, [OFFSET_PREFIX, MAX_ROWS_PREFIX, SHOW_ROWS_PREFIX]);
+ };
+
+ var _showSelectMessage = function(region, msg) {
+ if (region.showRecordSelectors) {
+ if (region.totalRows && region.totalRows !== region.selectedCount && region.selectedCount < MAX_SELECTION_SIZE) {
+ let text = 'Select All Rows';
+ if (region.totalRows > MAX_SELECTION_SIZE) {
+ text = `Select First ${MAX_SELECTION_SIZE.toLocaleString()} Rows`;
+ }
+ msg += " " + text + "";
+ }
+
+ msg += " " + "Select None";
+ var showOpts = [];
+ if (region.showRows !== 'all' && !_isMaxRowsAllRows(region))
+ showOpts.push("Show All");
+ if (region.showRows !== 'selected')
+ showOpts.push("Show Selected");
+ if (region.showRows !== 'unselected')
+ showOpts.push("Show Unselected");
+ msg += " " + showOpts.join(" ");
+ }
+
+ // add the record selector message, the link handlers will get added after render in _onRenderMessageArea
+ region.addMessage.call(region, msg, 'selection');
+ };
+
+ var _toggleAllRows = function(region, checked) {
+ var ids = [];
+
+ _getRowSelectors(region).each(function() {
+ if (!this.disabled) {
+ this.checked = checked;
+ ids.push(this.value);
+ }
+ });
+
+ _getAllRowSelectors(region).each(function() { this.checked = checked === true; });
+ return ids;
+ };
+
+ /**
+ * Asynchronous loader for a DataRegion
+ * @param region {DataRegion}
+ * @param [newParams] {string}
+ * @param [skipPrefixes] {string[]}
+ * @param [callback] {Function}
+ * @param [scope]
+ * @private
+ */
+ var _load = function(region, newParams, skipPrefixes, callback, scope) {
+
+ var params = _getAsyncParams(region, newParams ? newParams : _getParameters(region), skipPrefixes);
+ var jsonData = _getAsyncBody(region, params);
+
+ // TODO: This should be done in _getAsyncParams, but is not since _getAsyncBody relies on it. Refactor it.
+ // ensure SQL is not on the URL -- we allow any property to be pulled through when creating parameters.
+ if (params.sql) {
+ delete params.sql;
+ }
+
+ /**
+ * The target jQuery element that will be either written to or replaced
+ */
+ var target;
+
+ /**
+ * Flag used to determine if we should replace target element (default) or write to the target contents
+ * (used during QWP render for example)
+ * @type {boolean}
+ */
+ var useReplace = true;
+
+ /**
+ * The string identifier for where the region will render. Mainly used to display useful messaging upon failure.
+ * @type {string}
+ */
+ var renderEl;
+
+ if (region.renderTo) {
+ useReplace = false;
+ renderEl = region.renderTo;
+ target = $('#' + region.renderTo);
+ }
+ else if (!region.domId) {
+ throw '"renderTo" must be specified either upon construction or when calling render()';
+ }
+ else {
+ renderEl = region.domId;
+ target = $('#' + region.domId);
+
+ // attempt to find the correct node to render to...
+ var form = _getFormSelector(region);
+ if (form.length && form.parent('div').length) {
+ target = form.parent('div');
+ }
+ else {
+ // next best render target
+ throw 'unable to find a good target element. Perhaps this region is not using the standard renderer?'
+ }
+ }
+ var timerId = setTimeout(function() {
+ timerId = 0;
+ if (target) {
+ target.html("
" +
+ "
loading...
" +
+ "
");
+ }
+ }, 500);
+
+ LABKEY.Ajax.request({
+ timeout: region.timeout === undefined ? DEFAULT_TIMEOUT : region.timeout,
+ url: LABKEY.ActionURL.buildURL('project', 'getWebPart.api', region.containerPath),
+ method: 'POST',
+ params: params,
+ jsonData: jsonData,
+ success: function(response) {
+ if (timerId > 0) {
+ clearTimeout(timerId);//load mask task no longer needed
+ }
+ this.hidePanel(function() {
+ if (target.length) {
+
+ this.destroy();
+
+ LABKEY.Utils.loadAjaxContent(response, target, function() {
+
+ if ($.isFunction(callback)) {
+ callback.call(scope);
+ }
+
+ if ($.isFunction(this._success)) {
+ this._success.call(this.scope || this, this, response);
+ }
+
+ $(this).trigger('success', [this, response]);
+
+ this.RENDER_LOCK = true;
+ $(this).trigger('render', this);
+ this.RENDER_LOCK = false;
+ }, this, useReplace);
+ }
+ else {
+ // not finding element considered a failure
+ if ($.isFunction(this._failure)) {
+ this._failure.call(this.scope || this, response /* json */, response, undefined /* options */, target);
+ }
+ else if (!this.suppressRenderErrors) {
+ LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
+ }
+ }
+ }, this);
+ },
+ failure: LABKEY.Utils.getCallbackWrapper(function(json, response, options) {
+
+ if (target.length) {
+ if ($.isFunction(this._failure)) {
+ this._failure.call(this.scope || this, json, response, options);
+ }
+ else if (this.errorType === 'html') {
+ if (useReplace) {
+ target.replaceWith('
' + LABKEY.Utils.encodeHtml(json.exception) + '
');
+ }
+ else {
+ target.html('
' + LABKEY.Utils.encodeHtml(json.exception) + '
');
+ }
+ }
+ }
+ else if (!this.suppressRenderErrors) {
+ LABKEY.Utils.alert('Rendering Error', 'The element "' + renderEl + '" does not exist in the document. You may need to specify "renderTo".');
+ }
+ }, region, true),
+ scope: region
+ });
+
+ if (region.async && !region.complete && region.showPaginationCountAsync && !region.skipTotalRowCount) {
+ _loadAsyncTotalRowCount(region, params, jsonData);
+ }
+ region.skipTotalRowCount = false;
+ };
+
+ var totalRowCountRequests = {}; // track the request per region name so that we cancel the correct request when necessary
+ var _loadAsyncTotalRowCount = function(region, params, jsonData) {
+ // if there is a previous request pending, abort it before starting a new one
+ var totalRowCountRequest = totalRowCountRequests[region.name];
+ if (totalRowCountRequest !== undefined) {
+ totalRowCountRequest.abort();
+ }
+
+ region.totalRows = undefined;
+ region.loadingTotalRows = true;
+
+ totalRowCountRequests[region.name] = LABKEY.Query.selectRows({
+ ...region.getQueryConfig(),
+ method: 'POST',
+ containerPath: region.containerPath,
+ filterArray: LABKEY.Filter.getFiltersFromParameters({ ...params, ...jsonData.filters }, params.dataRegionName),
+ sort: undefined,
+ maxRows: 1,
+ offset: 0,
+ includeMetadata: false,
+ includeDetailsColumn: false,
+ includeUpdateColumn: false,
+ includeTotalCount: true,
+ success: function(json) {
+ totalRowCountRequests[region.name] = undefined;
+ region.loadingTotalRows = false;
+
+ if (json !== undefined && json.rowCount !== undefined) {
+ region.totalRows = json.rowCount;
+
+ // update the pagination button disabled state for 'Show Last' and 'Show All' since they include the totalRows count in their calc
+ var showLast = _showLastEnabled(region);
+ if (showLast) {
+ _getShowLastSelector(region).parent('li').removeClass('disabled');
+ _getShowAllSelector(region).parent('li').removeClass('disabled');
+ }
+ }
+ // note: use _getFormSelector instead of _getBarSelector so that we get the floating header as well
+ _getFormSelector(region).find('a.labkey-paginationText').html(_getPaginationText(region));
+ },
+ failure: function(error, request) {
+ var aborted = request.status === 0;
+ if (!aborted) {
+ console.error(error);
+ totalRowCountRequests[region.name] = undefined;
+ region.loadingTotalRows = false;
+ _getFormSelector(region).find('a.labkey-paginationText').html(_getPaginationText(region));
+ }
+ }
+ });
+ };
+
+ var _getAsyncBody = function(region, params) {
+ var json = {};
+
+ if (params.sql) {
+ json.sql = LABKEY.Utils.wafEncode(params.sql);
+ }
+
+ _processButtonBar(region, json);
+
+ // Issue 10505: add non-removable sorts and filters to json (not url params).
+ if (region.sort || region.filters || region.aggregates) {
+ json.filters = {};
+
+ if (region.filters) {
+ LABKEY.Filter.appendFilterParams(json.filters, region.filters, region.name);
+ }
+
+ if (region.sort) {
+ json.filters[region.dataRegionName + SORT_PREFIX] = region.sort;
+ }
+
+ if (region.aggregates) {
+ LABKEY.Filter.appendAggregateParams(json.filters, region.aggregates, region.name);
+ }
+ }
+
+ if (region.metadata) {
+ json.metadata = {
+ type: region.metadata.type,
+ value: LABKEY.Utils.wafEncode(region.metadata.value)
+ };
+ }
+
+ return json;
+ };
+
+ var _processButtonBar = function(region, json) {
+
+ var bar = region.buttonBar;
+
+ if (bar && (bar.position || (bar.items && bar.items.length > 0))) {
+ _processButtonBarItems(region, bar.items);
+
+ // only attach if valid
+ json.buttonBar = bar;
+ }
+ };
+
+ var _processButtonBarItems = function(region, items) {
+ if (LABKEY.Utils.isArray(items) && items.length > 0) {
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i];
+
+ if (item && $.isFunction(item.handler)) {
+ item.id = item.id || LABKEY.Utils.id();
+ // TODO: A better way? This exposed _onButtonClick isn't very awesome
+ item.onClick = "return LABKEY.DataRegions['" + region.name + "']._onButtonClick('" + item.id + "');";
+ }
+
+ if (item.items) {
+ _processButtonBarItems(region, item.items);
+ }
+ }
+ }
+ };
+
+ var _isFilter = function(region, parameter) {
+ return parameter && parameter.indexOf(region.name + '.') === 0 && parameter.indexOf('~') > 0;
+ };
+
+ var _getAsyncParams = function(region, newParams, skipPrefixes) {
+
+ var params = {};
+ var name = region.name;
+
+ //
+ // Certain parameters are only included if the region is 'async'. These
+ // were formerly a part of Query Web Part.
+ //
+ if (region.async) {
+ params[name + '.async'] = true;
+
+ if (LABKEY.Utils.isString(region.frame)) {
+ params['webpart.frame'] = region.frame;
+ }
+
+ if (LABKEY.Utils.isString(region.bodyClass)) {
+ params['webpart.bodyClass'] = region.bodyClass;
+ }
+
+ if (LABKEY.Utils.isString(region.title)) {
+ params['webpart.title'] = region.title;
+ }
+
+ if (LABKEY.Utils.isString(region.titleHref)) {
+ params['webpart.titleHref'] = region.titleHref;
+ }
+
+ if (LABKEY.Utils.isString(region.columns)) {
+ params[region.name + '.columns'] = region.columns;
+ }
+
+ _applyOptionalParameters(region, params, [
+ 'allowChooseQuery',
+ 'allowChooseView',
+ 'allowHeaderLock',
+ 'buttonBarPosition',
+ 'detailsURL',
+ 'deleteURL',
+ 'importURL',
+ 'insertURL',
+ 'linkTarget',
+ 'updateURL',
+ 'shadeAlternatingRows',
+ 'showBorders',
+ 'showDeleteButton',
+ 'showDetailsColumn',
+ 'showExportButtons',
+ 'showRStudioButton',
+ 'showImportDataButton',
+ 'showInsertNewButton',
+ 'showPagination',
+ 'showPaginationCount',
+ 'showReports',
+ 'showSurroundingBorder',
+ 'showFilterDescription',
+ 'showUpdateColumn',
+ 'showViewPanel',
+ 'timeout',
+ {name: 'disableAnalytics', prefix: true},
+ {name: 'maxRows', prefix: true, check: function(v) { return v > 0; }},
+ {name: 'showRows', prefix: true},
+ {name: 'offset', prefix: true, check: function(v) { return v !== 0; }},
+ {name: 'reportId', prefix: true},
+ {name: 'viewName', prefix: true}
+ ]);
+
+ // Sorts configured by the user when interacting with the grid. We need to pass these as URL parameters.
+ if (LABKEY.Utils.isString(region._userSort) && region._userSort.length > 0) {
+ params[name + SORT_PREFIX] = region._userSort;
+ }
+
+ if (region.userFilters) {
+ $.each(region.userFilters, function(filterExp, filterValue) {
+ if (params[filterExp] == undefined) {
+ params[filterExp] = [];
+ }
+ params[filterExp].push(filterValue);
+ });
+ region.userFilters = {}; // they've been applied
+ }
+
+ // TODO: Get rid of this and incorporate it with the normal containerFilter checks
+ if (region.userContainerFilter) {
+ params[name + CONTAINER_FILTER_NAME] = region.userContainerFilter;
+ }
+
+ if (region.parameters) {
+ var paramPrefix = name + PARAM_PREFIX;
+ $.each(region.parameters, function(parameter, value) {
+ var key = parameter;
+ if (parameter.indexOf(paramPrefix) !== 0) {
+ key = paramPrefix + parameter;
+ }
+ params[key] = value;
+ });
+ }
+ }
+
+ //
+ // apply all parameters
+ //
+
+ var newParamPrefixes = {};
+
+ if (newParams) {
+ newParams.forEach(function(pair) {
+ // Issue 25337: Filters may repeat themselves
+ if (_isFilter(region, pair[0])) {
+ if (params[pair[0]] == undefined) {
+ params[pair[0]] = [];
+ }
+ else if (!LABKEY.Utils.isArray(params[pair[0]])) {
+ params[pair[0]] = [params[pair[0]]];
+ }
+
+ var value = pair[1];
+
+ // Issue 47735: QWP date filter not being formatted
+ // This needs to be formatted for the response passed back to the grid for the filter display and
+ // filter dialog to render correctly
+ value = _ensureFilterDateFormat(value);
+
+ params[pair[0]].push(value);
+ }
+ else {
+ params[pair[0]] = pair[1];
+ }
+
+ newParamPrefixes[pair[0]] = true;
+ });
+ }
+
+ // Issue 40226: Don't include parameters that are being logically excluded
+ if (skipPrefixes) {
+ skipPrefixes.forEach(function(skipKey) {
+ if (params.hasOwnProperty(skipKey) && !newParamPrefixes.hasOwnProperty(skipKey)) {
+ delete params[skipKey];
+ }
+ });
+ }
+
+ //
+ // Properties that cannot be modified
+ //
+
+ params.dataRegionName = region.name;
+ params.schemaName = region.schemaName;
+ params.viewName = region.viewName;
+ params.reportId = region.reportId;
+ params.returnUrl = window.location.href;
+ params['webpart.name'] = 'Query';
+
+ if (region.queryName) {
+ params.queryName = region.queryName;
+ }
+ else if (region.sql) {
+ params.sql = region.sql;
+ }
+
+ var key = region.name + CONTAINER_FILTER_NAME;
+ var cf = region.getContainerFilter.call(region);
+ if (cf && !(key in params)) {
+ params[key] = cf;
+ }
+
+ return params;
+ };
+
+ var _updateFilter = function(region, filter, skipPrefixes) {
+ var params = [];
+ if (filter) {
+ params.push([filter.getURLParameterName(region.name), filter.getURLParameterValue()]);
+ }
+ _setParameters(region, params, [OFFSET_PREFIX].concat(skipPrefixes));
+ };
+
+ var _updateRequiresSelectionButtons = function(region, selectedCount) {
+
+ // update the 'select all on page' checkbox state
+ _getAllRowSelectors(region).each(function() {
+ if (region.isPageSelected.call(region)) {
+ this.checked = true;
+ this.indeterminate = false;
+ }
+ else if (region.selectedCount > 0) {
+ // There are rows selected, but the are not visible on this page.
+ this.checked = false;
+ this.indeterminate = true;
+ }
+ else {
+ this.checked = false;
+ this.indeterminate = false;
+ }
+ });
+
+ // 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);
+ }
+
+ // Issue 10566: for javascript perf on IE stash the requires selection buttons
+ if (!region._requiresSelectionButtons) {
+ // escape ', ", and \
+ var escaped = region.name.replace(/('|"|\\)/g, "\\$1");
+ region._requiresSelectionButtons = $("a[data-labkey-requires-selection='" + escaped + "']");
+ }
+
+ region._requiresSelectionButtons.each(function() {
+ var el = $(this);
+
+ var isDropdown = false;
+ var dropdownBtn = el.parent();
+ if (dropdownBtn && dropdownBtn.hasClass('lk-menu-drop') && dropdownBtn.hasClass('dropdown'))
+ isDropdown = true;
+
+ // handle min-count
+ var minCount = el.attr('data-labkey-requires-selection-min-count');
+ if (minCount) {
+ minCount = parseInt(minCount);
+ }
+ if (minCount === undefined) {
+ minCount = 1;
+ }
+
+ // handle max-count
+ var maxCount = el.attr('data-labkey-requires-selection-max-count');
+ if (maxCount) {
+ maxCount = parseInt(maxCount);
+ }
+
+ if (minCount <= selectedCount && (!maxCount || maxCount >= selectedCount)) {
+ el.removeClass('labkey-disabled-button');
+ if (isDropdown)
+ dropdownBtn.removeClass('labkey-disabled-button');
+ }
+ else {
+ el.addClass('labkey-disabled-button');
+ if (isDropdown)
+ dropdownBtn.addClass('labkey-disabled-button');
+ }
+ });
+ };
+
+ var HeaderLock = function(region) {
+
+ // init
+ if (!region.headerLock()) {
+ region._allowHeaderLock = false;
+ return;
+ }
+
+ this.region = region;
+
+ var table = $('#' + region.domId);
+ var firstRow = table.find('tr.labkey-alternate-row').first().children('td');
+
+ // If no data rows exist just turn off header locking
+ if (firstRow.length === 0) {
+ firstRow = table.find('tr.labkey-row').first().children('td');
+ if (firstRow.length === 0) {
+ region._allowHeaderLock = false;
+ return;
+ }
+ }
+
+ var headerRowId = region.domId + '-column-header-row';
+ var headerRow = $('#' + headerRowId);
+
+ if (headerRow.length === 0) {
+ region._allowHeaderLock = false;
+ return;
+ }
+
+ var BOTTOM_OFFSET = 100;
+
+ var me = this,
+ timeout,
+ locked = false,
+ lastLeft = 0,
+ pos = [ 0, 0, 0, 0 ],
+ domObserver = null;
+
+ // init
+ var floatRow = headerRow
+ .clone()
+ // TODO: Possibly namespace all the ids underneath
+ .attr('id', headerRowId + '-float')
+ .css({
+ 'box-shadow': '0 4px 4px #DCDCDC',
+ display: 'none',
+ position: 'fixed',
+ top: 0,
+ 'z-index': 2
+ });
+
+ floatRow.insertAfter(headerRow);
+
+ // respect showPagination but do not use it directly as it may change
+ var isPagingFloat = region.showPagination;
+ var floatPaging, floatPagingWidth = 0;
+
+ if (isPagingFloat) {
+ var pageWidget = _getBarSelector(region).find('.labkey-pagination');
+ if (pageWidget.children().length) {
+ floatPaging = $('')
+ .css({
+ 'background-color': 'white',
+ 'box-shadow': '0 4px 4px #DCDCDC',
+ display: 'none',
+ 'min-width': pageWidget.width(),
+ opacity: 0.7,
+ position: 'fixed',
+ top: floatRow.height(),
+ 'z-index': 1
+ })
+ .on('mouseover', function() {
+ $(this).css('opacity', '1.0');
+ })
+ .on('mouseout', function() {
+ $(this).css('opacity', '0.7')
+ });
+
+ var floatingPageWidget = pageWidget.clone(true).css('padding', '4px 8px');
+
+ // adjust padding when buttons aren't shown
+ if (!pageWidget.find('.btn-group').length) {
+ floatingPageWidget.css('padding-bottom', '8px')
+ }
+
+ floatPaging.append(floatingPageWidget);
+ table.parent().append(floatPaging);
+ floatPagingWidth = floatPaging.width();
+ } else {
+ isPagingFloat = false;
+ }
+ }
+
+ var disable = function() {
+ me.region._allowHeaderLock = false;
+
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+
+ $(window)
+ .unbind('load', domTask)
+ .unbind('resize', resizeTask)
+ .unbind('scroll', onScroll);
+
+ if (domObserver) {
+ domObserver.disconnect();
+ domObserver = null;
+ }
+ };
+
+ /**
+ * Configures the 'pos' array containing the following values:
+ * [0] - X-coordinate of the top of the object relative to the offset parent.
+ * [1] - Y-coordinate of the top of the object relative to the offset parent.
+ * [2] - Y-coordinate of the bottom of the object.
+ * [3] - width of the object
+ * This method assumes interaction with the Header of the Data Region.
+ */
+ var loadPosition = function() {
+ var header = headerRow.offset() || {top: 0};
+ var table = $('#' + region.domId);
+
+ var bottom = header.top + table.height() - BOTTOM_OFFSET;
+ var width = headerRow.width();
+ pos = [ header.left, header.top, bottom, width ];
+ };
+
+ loadPosition();
+
+ var onResize = function() {
+ loadPosition();
+ var sub_h = headerRow.find('th');
+
+ floatRow.width(headerRow.width()).find('th').each(function(i, el) {
+ $(el).width($(sub_h[i]).width());
+ });
+
+ isPagingFloat && floatPaging.css({
+ left: pos[0] - window.pageXOffset + floatRow.width() - floatPaging.width(),
+ top: floatRow.height()
+ });
+ };
+
+ /**
+ * WARNING: This function is called often. Performance implications for each line.
+ */
+ var onScroll = function() {
+ if (window.pageYOffset >= pos[1] && window.pageYOffset < pos[2]) {
+ var newLeft = pos[0] - window.pageXOffset;
+ var newPagingLeft = isPagingFloat ? newLeft + pos[3] - floatPagingWidth : 0;
+
+ var floatRowCSS = {
+ top: 0
+ };
+ var pagingCSS = isPagingFloat && {
+ top: floatRow.height()
+ };
+
+ if (!locked) {
+ locked = true;
+ floatRowCSS.display = 'table-row';
+ floatRowCSS.left = newLeft;
+
+ pagingCSS.display = 'block';
+ pagingCSS.left = newPagingLeft;
+ }
+ else if (lastLeft !== newLeft) {
+ floatRowCSS.left = newLeft;
+
+ pagingCSS.left = newPagingLeft;
+ }
+
+ floatRow.css(floatRowCSS);
+ isPagingFloat && floatPaging.css(pagingCSS);
+
+ lastLeft = newLeft;
+ }
+ else if (locked && window.pageYOffset >= pos[2]) {
+ var newTop = pos[2] - window.pageYOffset;
+
+ floatRow.css({
+ top: newTop
+ });
+
+ isPagingFloat && floatPaging.css({
+ top: newTop + floatRow.height()
+ });
+ }
+ else if (locked) {
+ locked = false;
+ floatRow.hide();
+ isPagingFloat && floatPaging.hide();
+ }
+ };
+
+ var resizeTask = function(immediate) {
+ clearTimeout(timeout);
+ if (immediate) {
+ onResize();
+ }
+ else {
+ timeout = setTimeout(onResize, 110);
+ }
+ };
+
+ var isDOMInit = false;
+
+ var domTask = function() {
+ if (!isDOMInit) {
+ isDOMInit = true;
+ // fire immediate to prevent flicker of components when reloading region
+ resizeTask(true);
+ }
+ else {
+ resizeTask();
+ }
+ onScroll();
+ };
+
+ $(window)
+ .one('load', domTask)
+ .on('resize', resizeTask)
+ .on('scroll', onScroll);
+
+ domObserver = new MutationObserver(mutationList =>
+ mutationList.filter(m => m.type === 'childList').forEach(m => {
+ m.addedNodes.forEach(domTask);
+ }));
+ domObserver.observe(document,{childList: true, subtree: true}); // Issue 13121, 50939
+
+ // ensure that resize/scroll fire at the end of initialization
+ domTask();
+
+ return {
+ disable: disable
+ }
+ };
+
+ //
+ // LOADER
+ //
+ LABKEY.DataRegion.create = function(config) {
+
+ var region = LABKEY.DataRegions[config.name];
+
+ if (region) {
+ // region already exists, update properties
+ $.each(config, function(key, value) {
+ region[key] = value;
+ });
+ if (!config.view) {
+ // when switching back to 'default' view, needs to clear region.view
+ region.view = undefined;
+ }
+ _init.call(region, config);
+ }
+ else {
+ // instantiate a new region
+ region = new LABKEY.DataRegion(config);
+ LABKEY.DataRegions[region.name] = region;
+ }
+
+ return region;
+ };
+
+ LABKEY.DataRegion.loadViewDesigner = function(cb, scope) {
+ LABKEY.requiresExt4Sandbox(function() {
+ LABKEY.requiresScript('internal/ViewDesigner', cb, scope);
+ });
+ };
+
+ LABKEY.DataRegion.getCustomViewEditableErrors = function(customView) {
+ var errors = [];
+ if (customView && !customView.editable) {
+ errors.push("The view is read-only and cannot be edited.");
+ }
+ return errors;
+ };
+
+ LABKEY.DataRegion.registerPane = function(regionName, callback, scope) {
+ var region = LABKEY.DataRegions[regionName];
+ if (region) {
+ callback.call(scope || region, region);
+ return;
+ }
+ else if (!_paneCache[regionName]) {
+ _paneCache[regionName] = [];
+ }
+
+ _paneCache[regionName].push({cb: callback, scope: scope});
+ };
+
+ LABKEY.DataRegion.selectAll = function(config) {
+ var params = {};
+ if (!config.url) {
+ // DataRegion doesn't have selectAllURL so generate url and query parameters manually
+ config.url = LABKEY.ActionURL.buildURL('query', 'selectAll.api', config.containerPath);
+
+ config.dataRegionName = config.dataRegionName || 'query';
+
+ params = LABKEY.Query.buildQueryParams(
+ config.schemaName,
+ config.queryName,
+ config.filters,
+ null,
+ config.dataRegionName
+ );
+
+ if (config.viewName)
+ params[config.dataRegionName + VIEWNAME_PREFIX] = config.viewName;
+
+ if (config.containerFilter)
+ params.containerFilter = config.containerFilter;
+
+ if (config.selectionKey)
+ params[config.dataRegionName + '.selectionKey'] = config.selectionKey;
+
+ $.each(config.parameters, function(propName, value) {
+ params[config.dataRegionName + PARAM_PREFIX + propName] = value;
+ });
+
+ if (config.ignoreFilter) {
+ params[config.dataRegionName + '.ignoreFilter'] = true;
+ }
+
+ // NOTE: ignore maxRows, showRows, and offset
+ }
+
+ LABKEY.Ajax.request({
+ 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)
+ });
+ };
+
+ /**
+ * Static method to add or remove items from the selection for a given {@link #selectionKey}.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {String} config.selectionKey See {@link #selectionKey}.
+ * @param {Array} config.ids Array of primary key ids for each row to select/unselect.
+ * @param {Boolean} config.checked If true, the ids will be selected, otherwise unselected.
+ * @param {Function} config.success The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'count' to indicate the updated selection count.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ *
+ * @see LABKEY.DataRegion#getSelected
+ * @see LABKEY.DataRegion#clearSelected
+ */
+ LABKEY.DataRegion.setSelected = function(config) {
+ LABKEY.Ajax.request({
+ url: LABKEY.ActionURL.buildURL('query', 'setSelected.api', config.containerPath),
+ method: 'POST',
+ jsonData: {
+ checked: config.checked,
+ id: config.ids || config.id,
+ key: config.selectionKey,
+ },
+ scope: config.scope,
+ success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
+ failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
+ });
+ };
+
+ /**
+ * Static method to clear all selected items for a given {@link #selectionKey}.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {String} config.selectionKey See {@link #selectionKey}.
+ * @param {Function} config.success The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'count' of 0 to indicate an empty selection.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ *
+ * @see LABKEY.DataRegion#setSelected
+ * @see LABKEY.DataRegion#getSelected
+ */
+ LABKEY.DataRegion.clearSelected = function(config) {
+ LABKEY.Ajax.request({
+ url: LABKEY.ActionURL.buildURL('query', 'clearSelected.api', config.containerPath),
+ method: 'POST',
+ jsonData: { key: config.selectionKey },
+ success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
+ failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
+ });
+ };
+
+ /**
+ * Static method to get all selected items for a given {@link #selectionKey}.
+ *
+ * @param config A configuration object with the following properties:
+ * @param {String} config.selectionKey See {@link #selectionKey}.
+ * @param {Function} config.success The function to be called upon success of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
data: an object with the property 'selected' that is an array of the primary keys for the selected rows.
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Function} [config.failure] The function to call upon error of the request.
+ * The callback will be passed the following parameters:
+ *
+ *
errorInfo: an object containing detailed error information (may be null)
+ *
response: The XMLHttpResponse object
+ *
+ * @param {Object} [config.scope] An optional scoping object for the success and error callback functions (default to this).
+ * @param {string} [config.containerPath] An alternate container path. If not specified, the current container path will be used.
+ * @param {boolean} [config.clearSelected] If true, clear the session-based selection for this Data Region after
+ * retrieving the current selection. Defaults to false.
+ *
+ * @see LABKEY.DataRegion#setSelected
+ * @see LABKEY.DataRegion#clearSelected
+ */
+ LABKEY.DataRegion.getSelected = function(config) {
+ var jsonData = { key: config.selectionKey };
+
+ // Issue 41705: Support clearing selection from getSelected()
+ if (config.clearSelected) {
+ jsonData.clearSelected = true;
+ }
+
+ LABKEY.Ajax.request({
+ url: LABKEY.ActionURL.buildURL('query', 'getSelected.api', config.containerPath),
+ method: 'POST',
+ jsonData: jsonData,
+ success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope),
+ failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true)
+ });
+ };
+
+ /**
+ * MessageArea wraps the display of messages in a DataRegion.
+ * @param dataRegion - The dataregion that the MessageArea will bind itself to.
+ * @param {String[]} [messages] - An initial messages object containing mappings of 'part' to 'msg'
+ * @constructor
+ */
+ var MessageArea = function(dataRegion, messages) {
+ this.bindRegion(dataRegion);
+
+ if (messages) {
+ this.setMessages(messages);
+ }
+ };
+
+ var MsgProto = MessageArea.prototype;
+
+ MsgProto.bindRegion = function(region) {
+ this.parentSel = '#' + region.domId + '-msgbox';
+ };
+
+ MsgProto.toJSON = function() {
+ return this.parts;
+ };
+
+ MsgProto.addMessage = function(msg, part, append) {
+ part = part || 'info';
+
+ var p = part.toLowerCase();
+ if (append && this.parts.hasOwnProperty(p))
+ {
+ this.parts[p] += msg;
+ this.render(p, msg);
+ }
+ else {
+ this.parts[p] = msg;
+ this.render(p);
+ }
+ };
+
+ MsgProto.getMessage = function(part) {
+ return this.parts[part.toLowerCase()];
+ };
+
+ MsgProto.hasMessage = function(part) {
+ return this.getMessage(part) !== undefined;
+ };
+
+ MsgProto.hasContent = function() {
+ return this.parts && Object.keys(this.parts).length > 0;
+ };
+
+ MsgProto.removeAll = function() {
+ this.parts = {};
+ this.render();
+ };
+
+ MsgProto.removeMessage = function(part) {
+ var p = part.toLowerCase();
+ if (this.parts.hasOwnProperty(p)) {
+ this.parts[p] = undefined;
+ this.render();
+ }
+ };
+
+ MsgProto.setMessages = function(messages) {
+ if (LABKEY.Utils.isObject(messages)) {
+ this.parts = messages;
+ }
+ else {
+ this.parts = {};
+ }
+ };
+
+ MsgProto.getParent = function() {
+ return $(this.parentSel);
+ };
+
+ MsgProto.render = function(partToUpdate, appendMsg) {
+ var hasMsg = false,
+ me = this,
+ parent = this.getParent();
+
+ $.each(this.parts, function(part, msg) {
+
+ if (msg) {
+ // If this is modified, update the server-side renderer in DataRegion.java renderMessages()
+ var partEl = parent.find('div[data-msgpart="' + part + '"]');
+ if (partEl.length === 0) {
+ parent.append([
+ '
',
+ msg,
+ '
'
+ ].join(''));
+ }
+ else if (partToUpdate !== undefined && partToUpdate === part) {
+ if (appendMsg !== undefined)
+ partEl.append(appendMsg);
+ else
+ partEl.html(msg)
+ }
+
+ hasMsg = true;
+ }
+ else {
+ parent.find('div[data-msgpart="' + part + '"]').remove();
+ delete me.parts[part];
+ }
+ });
+
+ if (hasMsg) {
+ this.show();
+ $(this).trigger('rendermsg', [this, this.parts]);
+ }
+ else {
+ this.hide();
+ parent.html('');
+ }
+ };
+
+ MsgProto.show = function() { this.getParent().show(); };
+ MsgProto.hide = function() { this.getParent().hide(); };
+ MsgProto.isVisible = function() { return $(this.parentSel + ':visible').length > 0; };
+ MsgProto.find = function(selector) {
+ return this.getParent().find('.dataregion_msgbox_ct').find(selector);
+ };
+ MsgProto.on = function(evt, callback, scope) { $(this).bind(evt, $.proxy(callback, scope)); };
+
+ /**
+ * @description Constructs a LABKEY.QueryWebPart class instance
+ * @class The LABKEY.QueryWebPart simplifies the task of dynamically adding a query web part to your page. Please use
+ * this class for adding query web parts to a page instead of {@link LABKEY.WebPart},
+ * which can be used for other types of web parts.
+ *
+ *
+ * @constructor
+ * @param {Object} config A configuration object with the following possible properties:
+ * @param {String} config.schemaName The name of the schema the web part will query.
+ * @param {String} config.queryName The name of the query within the schema the web part will select and display.
+ * @param {String} [config.viewName] the name of a saved view you wish to display for the given schema and query name.
+ * @param {String} [config.reportId] the report id of a saved report you wish to display for the given schema and query name.
+ * @param {Mixed} [config.renderTo] The element id, DOM element, or Ext element inside of which the part should be rendered. This is typically a <div>.
+ * If not supplied in the configuration, you must call the render() method to render the part into the page.
+ * @param {String} [config.errorType] A parameter to specify how query parse errors are returned. (default 'html'). Valid
+ * values are either 'html' or 'json'. If 'html' is specified the error will be rendered to an HTML view, if 'json' is specified
+ * the errors will be returned to the callback handlers as an array of objects named 'parseErrors' with the following properties:
+ *
+ *
msg: The error message.
+ *
line: The line number the error occurred at (optional).
+ *
col: The column number the error occurred at (optional).
+ *
errorStr: The line from the source query that caused the error (optional).
+ *
+ * @param {String} [config.sql] A SQL query that can be used instead of an existing schema name/query name combination.
+ * @param {Object} [config.metadata] Metadata that can be applied to the properties of the table fields. Currently, this option is only
+ * available if the query has been specified through the config.sql option. For full documentation on
+ * available properties, see LabKey XML Schema Reference.
+ * This object may contain the following properties:
+ *
+ *
type: The type of metadata being specified. Currently, only 'xml' is supported.
+ *
value: The metadata XML value as a string. For example: '<tables xmlns="http://labkey.org/data/xml"><table tableName="Announcement" tableDbType="NOT_IN_DB"><columns><column columnName="Title"><columnTitle>Custom Title</columnTitle></column></columns></table></tables>'
+ *
+ * @param {String} [config.title] A title for the web part. If not supplied, the query name will be used as the title.
+ * @param {String} [config.titleHref] If supplied, the title will be rendered as a hyperlink with this value as the href attribute.
+ * @param {String} [config.buttonBarPosition] DEPRECATED--see config.buttonBar.position
+ * @param {boolean} [config.allowChooseQuery] If the button bar is showing, whether or not it should be include a button
+ * to let the user choose a different query.
+ * @param {boolean} [config.allowChooseView] If the button bar is showing, whether or not it should be include a button
+ * to let the user choose a different view.
+ * @param {String} [config.detailsURL] Specify or override the default details URL for the table with one of the form
+ * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
+ * @param {boolean} [config.showDetailsColumn] If the underlying table has a details URL, show a column that renders a [details] link (default true). If true, the record selectors will be included regardless of the 'showRecordSelectors' config option.
+ * @param {String} [config.updateURL] Specify or override the default updateURL for the table with one of the form
+ * "/controller/action.view?id=${RowId}" or "org.labkey.package.MyController$ActionAction.class?id=${RowId}"
+ * @param {boolean} [config.showUpdateColumn] If the underlying table has an update URL, show a column that renders an [edit] link (default true).
+ * @param {String} [config.insertURL] Specify or override the default insert URL for the table with one of the form
+ * "/controller/insertAction.view" or "org.labkey.package.MyController$InsertActionAction.class"
+ * @param {String} [config.importURL] Specify or override the default bulk import URL for the table with one of the form
+ * "/controller/importAction.view" or "org.labkey.package.MyController$ImportActionAction.class"
+ * @param {String} [config.deleteURL] Specify or override the default delete URL for the table with one of the form
+ * "/controller/action.view" or "org.labkey.package.MyController$ActionAction.class". The keys for the selected rows
+ * will be included in the POST.
+ * @param {boolean} [config.showImportDataButton] If the underlying table has an import URL, show an "Import Bulk Data" button in the button bar (default true).
+ * @param {boolean} [config.showInsertNewButton] If the underlying table has an insert URL, show an "Insert New" button in the button bar (default true).
+ * @param {boolean} [config.showDeleteButton] Show a "Delete" button in the button bar (default true).
+ * @param {boolean} [config.showReports] If true, show reports on the Views menu (default true).
+ * @param {boolean} [config.showExportButtons] Show the export button menu in the button bar (default true).
+ * @param {boolean} [config.showRStudioButton] Show the export to RStudio button menu in the button bar. Requires export button to work. (default false).
+ * @param {boolean} [config.showBorders] Render the table with borders (default true).
+ * @param {boolean} [config.showSurroundingBorder] Render the table with a surrounding border (default true).
+ * @param {boolean} [config.showFilterDescription] Include filter and parameter values in the grid header, if present (default true).
+ * @param {boolean} [config.showRecordSelectors] Render the select checkbox column (default undefined, meaning they will be shown if the query is updatable by the current user).
+ * Both 'showDeleteButton' and 'showExportButtons' must be set to false for the 'showRecordSelectors = false' setting to hide the checkboxes.
+ * @param {boolean} [config.showPagination] Show the pagination links and count (default true).
+ * @param {boolean} [config.showPaginationCount] Show the total count of rows in the pagination information text (default true).
+ * @param {boolean} [config.showPaginationCountAsync] Show the total count of rows in the pagination information text, but query for it asynchronously so that the grid data can load initially without it (default false).
+ * @param {boolean} [config.shadeAlternatingRows] Shade every other row with a light gray background color (default true).
+ * @param {boolean} [config.suppressRenderErrors] If true, no alert will appear if there is a problem rendering the QueryWebpart. This is most often encountered if page configuration changes between the time when a request was made and the content loads. Defaults to false.
+ * @param {Object} [config.buttonBar] Button bar configuration. This object may contain any of the following properties:
+ *
+ *
position: Configures where the button bar will appear with respect to the data grid: legal values are 'top', or 'none'. Default is 'top'.
+ *
includeStandardButtons: If true, all standard buttons not specifically mentioned in the items array will be included at the end of the button bar. Default is false.
+ *
items: An array of button bar items. Each item may be either a reference to a standard button, or a new button configuration.
+ * to reference standard buttons, use one of the properties on {@link #standardButtons}, or simply include a string
+ * that matches the button's caption. To include a new button configuration, create an object with the following properties:
+ *
+ *
text: The text you want displayed on the button (aka the caption).
+ *
url: The URL to navigate to when the button is clicked. You may use LABKEY.ActionURL to build URLs to controller actions.
+ * Specify this or a handler function, but not both.
+ *
handler: A reference to the JavaScript function you want called when the button is clicked.
+ *
permission: Optional. Permission that the current user must possess to see the button.
+ * Valid options are 'READ', 'INSERT', 'UPDATE', 'DELETE', and 'ADMIN'.
+ * Default is 'READ' if permissionClass is not specified.
+ *
permissionClass: Optional. If permission (see above) is not specified, the fully qualified Java class
+ * name of the permission that the user must possess to view the button.
+ *
requiresSelection: A boolean value (true/false) indicating whether the button should only be enabled when
+ * data rows are checked/selected.
+ *
items: To create a drop-down menu button, set this to an array of menu item configurations.
+ * Each menu item configuration can specify any of the following properties:
+ *
+ *
text: The text of the menu item.
+ *
handler: A reference to the JavaScript function you want called when the menu item is clicked.
+ *
icon: A url to an image to use as the menu item's icon.
+ *
items: An array of sub-menu item configurations. Used for fly-out menus.
+ *
+ *
+ *
+ *
+ *
+ * @param {String} [config.columns] Comma-separated list of column names to be shown in the grid, overriding
+ * whatever might be set in a custom view.
+ * @param {String} [config.sort] A base sort order to use. This is a comma-separated list of column names, each of
+ * which may have a - prefix to indicate a descending sort. It will be treated as the final sort, after any that the user
+ * has defined in a custom view or through interacting with the grid column headers.
+ * @param {String} [config.removeableSort] An additional sort order to use. This is a comma-separated list of column names, each of
+ * which may have a - prefix to indicate a descending sort. It will be treated as the first sort, before any that the user
+ * has defined in a custom view or through interacting with the grid column headers.
+ * @param {Array} [config.filters] A base set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
+ * each of which is created using the {@link LABKEY.Filter.create} method. These filters cannot be removed by the user
+ * interacting with the UI.
+ * For compatibility with the {@link LABKEY.Query} object, you may also specify base filters using config.filterArray.
+ * @param {Array} [config.removeableFilters] A set of filters to apply. This should be an array of {@link LABKEY.Filter} objects
+ * each of which is created using the {@link LABKEY.Filter.create} method. These filters can be modified or removed by the user
+ * interacting with the UI.
+ * @param {Object} [config.parameters] Map of name (string)/value pairs for the values of parameters if the SQL
+ * references underlying queries that are parameterized. For example, the following passes two parameters to the query: {'Gender': 'M', 'CD4': '400'}.
+ * The parameters are written to the request URL as follows: query.param.Gender=M&query.param.CD4=400. For details on parameterized SQL queries, see
+ * Parameterized SQL Queries.
+ * @param {Array} [config.aggregates] An array of aggregate definitions. The objects in this array should have the properties:
+ *
+ *
column: The name of the column to be aggregated.
+ *
type: The aggregate type (see {@link LABKEY.AggregateTypes})
+ *
label: Optional label used when rendering the aggregate row.
+ *
+ * @param {String} [config.showRows] Either 'paginated' (the default) 'selected', 'unselected', 'all', or 'none'.
+ * When 'paginated', the maxRows and offset parameters can be used to page through the query's result set rows.
+ * When 'selected' or 'unselected' the set of rows selected or unselected by the user in the grid view will be returned.
+ * You can programmatically get and set the selection using the {@link LABKEY.DataRegion.setSelected} APIs.
+ * Setting config.maxRows to -1 is the same as 'all'
+ * and setting config.maxRows to 0 is the same as 'none'.
+ * @param {Integer} [config.maxRows] The maximum number of rows to return from the server (defaults to 100).
+ * If you want to return all possible rows, set this config property to -1.
+ * @param {Integer} [config.offset] The index of the first row to return from the server (defaults to 0).
+ * Use this along with the maxRows config property to request pages of data.
+ * @param {String} [config.dataRegionName] The name to be used for the data region. This should be unique within
+ * the set of query views on the page. If not supplied, a unique name is generated for you.
+ * @param {String} [config.linkTarget] The name of a browser window/tab in which to open URLs rendered in the
+ * QueryWebPart. If not supplied, links will generally be opened in the same browser window/tab where the QueryWebPart.
+ * @param {String} [config.frame] The frame style to use for the web part. This may be one of the following:
+ * 'div', 'portal', 'none', 'dialog', 'title', 'left-nav'.
+ * @param {String} [config.showViewPanel] Open the customize view panel after rendering. The value of this option can be "true" or one of "ColumnsTab", "FilterTab", or "SortTab".
+ * @param {String} [config.bodyClass] A CSS style class that will be added to the enclosing element for the web part.
+ * Note, this may not be applied when used in conjunction with some "frame" types (e.g. 'none').
+ * @param {Function} [config.success] A function to call after the part has been rendered. It will be passed two arguments:
+ *
+ *
dataRegion: the LABKEY.DataRegion object representing the rendered QueryWebPart
+ *
request: the XMLHTTPRequest that was issued to the server
+ *
+ * @param {Function} [config.failure] A function to call if the request to retrieve the content fails. It will be passed three arguments:
+ *
+ *
json: JSON object containing the exception.
+ *
response: The XMLHttpRequest object containing the response data.
+ *
options: The parameter to the request call.
+ *
+ * @param {Object} [config.scope] An object to use as the callback function's scope. Defaults to this.
+ * @param {int} [config.timeout] A timeout for the AJAX call, in milliseconds. Default is 30000 (30 seconds).
+ * @param {String} [config.containerPath] The container path in which the schema and query name are defined. If not supplied, the current container path will be used.
+ * @param {String} [config.containerFilter] One of the values of {@link LABKEY.Query.containerFilter} that sets the scope of this query. If not supplied, the current folder will be used.
+ * @example
+ * <div id='queryTestDiv1'/>
+ * <script type="text/javascript">
+ var qwp1 = new LABKEY.QueryWebPart({
+
+ renderTo: 'queryTestDiv1',
+ title: 'My Query Web Part',
+ schemaName: 'lists',
+ queryName: 'People',
+ buttonBarPosition: 'none',
+ aggregates: [
+ {column: 'First', type: LABKEY.AggregateTypes.COUNT, label: 'Total People'},
+ {column: 'Age', type: LABKEY.AggregateTypes.MEAN}
+ ],
+ filters: [
+ LABKEY.Filter.create('Last', 'Flintstone')
+ ],
+ sort: '-Last'
+ });
+
+ //note that you may also register for the 'render' event
+ //instead of using the success config property.
+ //registering for events is done using Ext event registration.
+ //Example:
+ qwp1.on("render", onRender);
+ function onRender()
+ {
+ //...do something after the part has rendered...
+ }
+
+ ///////////////////////////////////////
+ // Custom Button Bar Example
+
+ var qwp1 = new LABKEY.QueryWebPart({
+ renderTo: 'queryTestDiv1',
+ title: 'My Query Web Part',
+ schemaName: 'lists',
+ queryName: 'People',
+ buttonBar: {
+ includeStandardButtons: true,
+ items:[
+ LABKEY.QueryWebPart.standardButtons.views,
+ {text: 'Test', url: LABKEY.ActionURL.buildURL('project', 'begin')},
+ {text: 'Test Script', onClick: "alert('Hello World!'); return false;"},
+ {text: 'Test Handler', handler: onTestHandler},
+ {text: 'Test Menu', items: [
+ {text: 'Item 1', handler: onItem1Handler},
+ {text: 'Fly Out', items: [
+ {text: 'Sub Item 1', handler: onItem1Handler}
+ ]},
+ '-', //separator
+ {text: 'Item 2', handler: onItem2Handler}
+ ]},
+ LABKEY.QueryWebPart.standardButtons.exportRows
+ ]}
+ });
+
+ function onTestHandler(dataRegion)
+ {
+ alert("onTestHandler called!");
+ return false;
+ }
+
+ function onItem1Handler(dataRegion)
+ {
+ alert("onItem1Handler called!");
+ }
+
+ function onItem2Handler(dataRegion)
+ {
+ alert("onItem2Handler called!");
+ }
+
+ </script>
+ */
+ LABKEY.QueryWebPart = function(config) {
+ config._useQWPDefaults = true;
+ return LABKEY.DataRegion.create(config);
+ };
+})(jQuery);
+
+/**
+ * A read-only object that exposes properties representing standard buttons shown in LabKey data grids.
+ * These are used in conjunction with the buttonBar configuration. The following buttons are currently defined:
+ *
+ *
LABKEY.QueryWebPart.standardButtons.query
+ *
LABKEY.QueryWebPart.standardButtons.views
+ *
LABKEY.QueryWebPart.standardButtons.charts
+ *
LABKEY.QueryWebPart.standardButtons.insertNew
+ *
LABKEY.QueryWebPart.standardButtons.deleteRows
+ *
LABKEY.QueryWebPart.standardButtons.exportRows
+ *
LABKEY.QueryWebPart.standardButtons.print
+ *
+ * @name standardButtons
+ * @memberOf LABKEY.QueryWebPart#
+ */
+LABKEY.QueryWebPart.standardButtons = {
+ query: 'query',
+ views: 'grid views',
+ charts: 'charts',
+ insertNew: 'insert',
+ deleteRows: 'delete',
+ exportRows: 'export',
+ print: 'print'
+};
+
+/**
+ * Requests the query web part content and renders it within the element identified by the renderTo parameter.
+ * Note that you do not need to call this method explicitly if you specify a renderTo property on the config object
+ * handed to the class constructor. If you do not specify renderTo in the config, then you must call this method
+ * passing the id of the element in which you want the part rendered
+ * @function
+ * @param renderTo The id of the element in which you want the part rendered.
+ */
+
+LABKEY.QueryWebPart.prototype.render = LABKEY.DataRegion.prototype.render;
+
+/**
+ * @returns {LABKEY.DataRegion}
+ */
+LABKEY.QueryWebPart.prototype.getDataRegion = LABKEY.DataRegion.prototype.getDataRegion;
+
+LABKEY.AggregateTypes = {
+ /**
+ * Displays the sum of the values in the specified column
+ */
+ SUM: 'sum',
+ /**
+ * Displays the mean of the values in the specified column
+ */
+ MEAN: 'mean',
+ /**
+ * Displays the count of the non-blank values in the specified column
+ */
+ COUNT: 'count',
+ /**
+ * Displays the maximum value from the specified column
+ */
+ MIN: 'min',
+ /**
+ * Displays the minimum values from the specified column
+ */
+ MAX: 'max',
+
+ /**
+ * Deprecated
+ */
+ AVG: 'mean'
+
+ // TODO how to allow premium module additions to aggregate types?
+};
From ce01af40b0f1ad0f382970a86dea900d53abcbc1 Mon Sep 17 00:00:00 2001
From: labkey-nicka
Date: Tue, 7 Oct 2025 11:34:22 -0700
Subject: [PATCH 2/7] Max selection size for module context
---
.../labkey/api/data/DataRegionSelection.java | 75 +-
.../org/labkey/api/query/QueryService.java | 1 +
query/src/org/labkey/query/QueryModule.java | 2 +
.../query/controllers/QueryController.java | 17548 ++++++++--------
4 files changed, 8828 insertions(+), 8798 deletions(-)
diff --git a/api/src/org/labkey/api/data/DataRegionSelection.java b/api/src/org/labkey/api/data/DataRegionSelection.java
index 987341e3b1c..7495d0af2e6 100644
--- a/api/src/org/labkey/api/data/DataRegionSelection.java
+++ b/api/src/org/labkey/api/data/DataRegionSelection.java
@@ -46,7 +46,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -61,7 +60,9 @@ public class DataRegionSelection
public static final String SELECTED_VALUES = ".selectValues";
public static final String SEPARATOR = "$";
public static final String DATA_REGION_SELECTION_KEY = "dataRegionSelectionKey";
- public static final int MAX_SELECTION_SIZE = 1_000;
+
+ // Issue 53997: Establish a maximum number of selected items allowed for a query.
+ public static final int MAX_QUERY_SELECTION_SIZE = 100_000;
// set/updated using query-setSnapshotSelection
// can be used to hold an arbitrary set of selections in session
@@ -211,7 +212,6 @@ public static String getSelectionKeyFromRequest(ViewContext context)
Set result = new LinkedHashSet<>(parameterSelected);
Set sessionSelected = getSet(context, key, false);
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionSelected)
{
result.addAll(sessionSelected);
@@ -268,22 +268,54 @@ public static int setSelected(ViewContext context, String key, Collection selection, boolean checked, boolean useSnapshot)
{
- if (checked && selection.size() > MAX_SELECTION_SIZE)
- throw new BadRequestException(String.format("Too many selected items: %s. Maximum number of selected items allowed is %s.", Formats.commaf0.format(selection.size()), Formats.commaf0.format(MAX_SELECTION_SIZE)));
+ if (checked && selection.size() > MAX_QUERY_SELECTION_SIZE)
+ throw new BadRequestException(selectionTooLargeMessage(selection.size()));
Set selectedValues = getSet(context, key, true, useSnapshot);
if (checked)
{
- // TODO: Need to synchronize on this change and not make it if it is too many
+ // Verify that adding these selections will not result in a set that is too large
+ if (selectedValues.size() + selection.size() > MAX_QUERY_SELECTION_SIZE)
+ {
+ // Do not modify the actual selected values
+ int current = selectedValues.size();
+ int distinctAdds = 0;
+
+ for (String id : selection)
+ {
+ if (!selectedValues.contains(id))
+ distinctAdds++;
+ }
+
+ int prospective = current + distinctAdds;
+ if (prospective > MAX_QUERY_SELECTION_SIZE)
+ throw new BadRequestException(selectionTooLargeMessage(prospective));
+ }
+
selectedValues.addAll(selection);
- if (selectedValues.size() > MAX_SELECTION_SIZE)
- throw new BadRequestException(String.format("Too many selected items: %s. Maximum number of selected items allowed is %s.", Formats.commaf0.format(selectedValues.size()), Formats.commaf0.format(MAX_SELECTION_SIZE)));
}
else
selectedValues.removeAll(selection);
+
return selectedValues.size();
}
+ public static int setSelectedFromForm(QueryForm form)
+ {
+ var view = getQueryView(form);
+ var viewContext = view.getViewContext();
+ var selection = getSet(viewContext, form.getQuerySettings().getSelectionKey(), true);
+ var items = getSelectedItems(view, selection);
+
+ return setSelected(viewContext, form.getQuerySettings().getSelectionKey(), items, false);
+ }
+
+ private static String selectionTooLargeMessage(long size)
+ {
+ return String.format("Too many selected items: %s. Maximum number of selected items allowed is %s.",
+ Formats.commaf0.format(size), Formats.commaf0.format(MAX_QUERY_SELECTION_SIZE));
+ }
+
/**
* Clear any session attributes that match the given container path, as the prefix, and the selection key, as the suffix
*/
@@ -347,13 +379,11 @@ public static void clearAll(ViewContext context)
* Gets the ids of the selected items for all items in the given query form's view. That is,
* not just the items on the current page, but all selected items corresponding to the view's filters.
*/
- public static List getSelected(QueryForm form, boolean clearSelected) throws IOException
+ public static Set getSelected(QueryForm form, boolean clearSelected) throws IOException
{
- List items;
var view = getQueryView(form);
-
var selection = getSet(view.getViewContext(), form.getQuerySettings().getSelectionKey(), true);
- items = getSelectedItems(view, selection);
+ var items = getSelectedItems(view, selection);
if (clearSelected && !selection.isEmpty())
{
@@ -363,7 +393,8 @@ public static List getSelected(QueryForm form, boolean clearSelected) th
items.forEach(selection::remove);
}
}
- return Collections.unmodifiableList(items);
+
+ return items;
}
private static Pair getDataRegionContext(QueryView view)
@@ -407,7 +438,7 @@ private static Pair getDataRegionContext(QueryView vi
return schema.createView(form, null);
}
- public static List getValidatedIds(@NotNull List selection, QueryForm form) throws IOException
+ public static Set getValidatedIds(@NotNull Collection selection, QueryForm form)
{
return getSelectedItems(getQueryView(form), selection);
}
@@ -454,7 +485,7 @@ public static int setSelectionForAll(QueryView view, String key, boolean checked
try (Timing ignored = MiniProfiler.step("selectAll"); ResultSet rs = rgn.getResults(rc))
{
- var selection = createSelectionList(rc, rgn, rs, null);
+ var selection = createSelectionSet(rc, rgn, rs, null);
return setSelected(view.getViewContext(), key, selection, checked);
}
catch (SQLException e)
@@ -468,13 +499,13 @@ public static int setSelectionForAll(QueryView view, String key, boolean checked
* @param view the view from which to retrieve the data region context and session variable
* @param selectedValues optionally (nullable) specify a collection of selected values that will be matched
* against when selecting items. If null, then all items will be returned.
- * @return list of items from the result set that are in the selected session, or an empty list if none.
+ * @return Set of items from the result set that are in the selected session, or an empty list if none.
*/
- private static List getSelectedItems(QueryView view, @NotNull Collection selectedValues) throws IOException
+ private static Set getSelectedItems(QueryView view, @NotNull Collection selectedValues)
{
// Issue 48657: no need to query the region result set if we have no selectedValues
if (selectedValues.isEmpty())
- return new LinkedList<>();
+ return new LinkedHashSet<>();
var dataRegionContext = getDataRegionContext(view);
var rgn = dataRegionContext.first;
@@ -494,7 +525,7 @@ private static List getSelectedItems(QueryView view, @NotNull Collection
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (selectedValues)
{
- return createSelectionList(ctx, rgn, rs, selectedValues);
+ return createSelectionSet(ctx, rgn, rs, selectedValues);
}
}
catch (SQLException e)
@@ -503,14 +534,14 @@ private static List getSelectedItems(QueryView view, @NotNull Collection
}
}
- private static List createSelectionList(
+ private static Set createSelectionSet(
RenderContext ctx,
DataRegion rgn,
ResultSet rs,
@Nullable Collection selectedValues
) throws SQLException
{
- List selected = new ArrayList<>();
+ Set selected = new LinkedHashSet<>();
if (rs != null)
{
@@ -526,7 +557,7 @@ private static List createSelectionList(
if (selectedValues == null || selectedValues.contains(value))
{
selected.add(value);
- if (selected.size() == MAX_SELECTION_SIZE)
+ if (selected.size() == MAX_QUERY_SELECTION_SIZE)
break;
}
}
diff --git a/api/src/org/labkey/api/query/QueryService.java b/api/src/org/labkey/api/query/QueryService.java
index 844b9cb3db6..bbccad189f6 100644
--- a/api/src/org/labkey/api/query/QueryService.java
+++ b/api/src/org/labkey/api/query/QueryService.java
@@ -56,6 +56,7 @@ public interface QueryService
String EXPERIMENTAL_LAST_MODIFIED = "queryMetadataLastModified";
String EXPERIMENTAL_PRODUCT_ALL_FOLDER_LOOKUPS = "queryProductAllFolderLookups";
String EXPERIMENTAL_PRODUCT_PROJECT_DATA_LISTING_SCOPED = "queryProductProjectDataListingScoped";
+ String MAX_QUERY_SELECTION = "maxQuerySelection";
String PRODUCT_FOLDERS_ENABLED = "isProductFoldersEnabled";
String PRODUCT_FOLDERS_EXIST = "hasProductFolders";
String USE_ROW_BY_ROW_UPDATE = "useLegacyUpdateRows";
diff --git a/query/src/org/labkey/query/QueryModule.java b/query/src/org/labkey/query/QueryModule.java
index 16f6c82957c..7485cfe98ca 100644
--- a/query/src/org/labkey/query/QueryModule.java
+++ b/query/src/org/labkey/query/QueryModule.java
@@ -25,6 +25,7 @@
import org.labkey.api.data.Aggregate;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerManager;
+import org.labkey.api.data.DataRegionSelection;
import org.labkey.api.data.JdbcType;
import org.labkey.api.data.views.DataViewService;
import org.labkey.api.exp.property.PropertyService;
@@ -416,6 +417,7 @@ public JSONObject getPageContextJson(ContainerUser context)
json.put(QueryService.PRODUCT_FOLDERS_EXIST, isProductFoldersEnabled && container.hasProductFolders());
json.put(QueryService.EXPERIMENTAL_PRODUCT_ALL_FOLDER_LOOKUPS, QueryService.get().isProductFoldersAllFolderScopeEnabled());
json.put(QueryService.EXPERIMENTAL_PRODUCT_PROJECT_DATA_LISTING_SCOPED, QueryService.get().isProductFoldersDataListingScopedToProject());
+ json.put(QueryService.MAX_QUERY_SELECTION, DataRegionSelection.MAX_QUERY_SELECTION_SIZE);
return json;
}
}
diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java
index 258eb3d0b91..5dd88b4921e 100644
--- a/query/src/org/labkey/query/controllers/QueryController.java
+++ b/query/src/org/labkey/query/controllers/QueryController.java
@@ -1,8776 +1,8772 @@
-/*
- * Copyright (c) 2008-2019 LabKey Corporation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.labkey.query.controllers;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpSession;
-import org.antlr.runtime.tree.Tree;
-import org.apache.commons.beanutils.ConversionException;
-import org.apache.commons.beanutils.ConvertUtils;
-import org.apache.commons.collections4.MultiValuedMap;
-import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
-import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Strings;
-import org.apache.commons.lang3.mutable.MutableInt;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.poi.ss.usermodel.Workbook;
-import org.apache.xmlbeans.XmlError;
-import org.apache.xmlbeans.XmlException;
-import org.apache.xmlbeans.XmlOptions;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.json.JSONParserConfiguration;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.labkey.api.action.Action;
-import org.labkey.api.action.ActionType;
-import org.labkey.api.action.ApiJsonForm;
-import org.labkey.api.action.ApiJsonWriter;
-import org.labkey.api.action.ApiQueryResponse;
-import org.labkey.api.action.ApiResponse;
-import org.labkey.api.action.ApiResponseWriter;
-import org.labkey.api.action.ApiSimpleResponse;
-import org.labkey.api.action.ApiUsageException;
-import org.labkey.api.action.ApiVersion;
-import org.labkey.api.action.ConfirmAction;
-import org.labkey.api.action.ExportAction;
-import org.labkey.api.action.ExportException;
-import org.labkey.api.action.ExtendedApiQueryResponse;
-import org.labkey.api.action.FormHandlerAction;
-import org.labkey.api.action.FormViewAction;
-import org.labkey.api.action.HasBindParameters;
-import org.labkey.api.action.JsonInputLimit;
-import org.labkey.api.action.LabKeyError;
-import org.labkey.api.action.Marshal;
-import org.labkey.api.action.Marshaller;
-import org.labkey.api.action.MutatingApiAction;
-import org.labkey.api.action.NullSafeBindException;
-import org.labkey.api.action.ReadOnlyApiAction;
-import org.labkey.api.action.ReportingApiQueryResponse;
-import org.labkey.api.action.SimpleApiJsonForm;
-import org.labkey.api.action.SimpleErrorView;
-import org.labkey.api.action.SimpleRedirectAction;
-import org.labkey.api.action.SimpleViewAction;
-import org.labkey.api.action.SpringActionController;
-import org.labkey.api.admin.AdminUrls;
-import org.labkey.api.attachments.SpringAttachmentFile;
-import org.labkey.api.audit.AbstractAuditTypeProvider;
-import org.labkey.api.audit.AuditLogService;
-import org.labkey.api.audit.AuditTypeEvent;
-import org.labkey.api.audit.TransactionAuditProvider;
-import org.labkey.api.audit.provider.ContainerAuditProvider;
-import org.labkey.api.collections.CaseInsensitiveHashMap;
-import org.labkey.api.collections.CaseInsensitiveHashSet;
-import org.labkey.api.collections.IntHashMap;
-import org.labkey.api.collections.LabKeyCollectors;
-import org.labkey.api.collections.RowMapFactory;
-import org.labkey.api.collections.Sets;
-import org.labkey.api.data.AbstractTableInfo;
-import org.labkey.api.data.ActionButton;
-import org.labkey.api.data.Aggregate;
-import org.labkey.api.data.AnalyticsProviderItem;
-import org.labkey.api.data.BaseColumnInfo;
-import org.labkey.api.data.ButtonBar;
-import org.labkey.api.data.CachedResultSets;
-import org.labkey.api.data.ColumnHeaderType;
-import org.labkey.api.data.ColumnInfo;
-import org.labkey.api.data.CompareType;
-import org.labkey.api.data.Container;
-import org.labkey.api.data.ContainerFilter;
-import org.labkey.api.data.ContainerManager;
-import org.labkey.api.data.ContainerType;
-import org.labkey.api.data.DataRegion;
-import org.labkey.api.data.DataRegionSelection;
-import org.labkey.api.data.DbSchema;
-import org.labkey.api.data.DbSchemaType;
-import org.labkey.api.data.DbScope;
-import org.labkey.api.data.DisplayColumn;
-import org.labkey.api.data.ExcelWriter;
-import org.labkey.api.data.ForeignKey;
-import org.labkey.api.data.JdbcMetaDataSelector;
-import org.labkey.api.data.JdbcType;
-import org.labkey.api.data.JsonWriter;
-import org.labkey.api.data.PHI;
-import org.labkey.api.data.PropertyManager;
-import org.labkey.api.data.PropertyManager.PropertyMap;
-import org.labkey.api.data.PropertyManager.WritablePropertyMap;
-import org.labkey.api.data.PropertyStorageSpec;
-import org.labkey.api.data.QueryLogging;
-import org.labkey.api.data.ResultSetView;
-import org.labkey.api.data.RuntimeSQLException;
-import org.labkey.api.data.SQLFragment;
-import org.labkey.api.data.SchemaTableInfo;
-import org.labkey.api.data.ShowRows;
-import org.labkey.api.data.SimpleFilter;
-import org.labkey.api.data.SqlSelector;
-import org.labkey.api.data.TSVWriter;
-import org.labkey.api.data.Table;
-import org.labkey.api.data.TableInfo;
-import org.labkey.api.data.TableSelector;
-import org.labkey.api.data.VirtualTable;
-import org.labkey.api.data.dialect.JdbcMetaDataLocator;
-import org.labkey.api.data.dialect.SqlDialect;
-import org.labkey.api.dataiterator.DataIteratorBuilder;
-import org.labkey.api.dataiterator.DataIteratorContext;
-import org.labkey.api.dataiterator.DataIteratorUtil;
-import org.labkey.api.dataiterator.DetailedAuditLogDataIterator;
-import org.labkey.api.dataiterator.ListofMapsDataIterator;
-import org.labkey.api.exceptions.OptimisticConflictException;
-import org.labkey.api.exp.ExperimentException;
-import org.labkey.api.exp.api.ProvenanceRecordingParams;
-import org.labkey.api.exp.api.ProvenanceService;
-import org.labkey.api.exp.list.ListDefinition;
-import org.labkey.api.exp.list.ListService;
-import org.labkey.api.exp.property.Domain;
-import org.labkey.api.exp.property.DomainAuditProvider;
-import org.labkey.api.exp.property.DomainKind;
-import org.labkey.api.exp.property.PropertyService;
-import org.labkey.api.files.FileContentService;
-import org.labkey.api.gwt.client.AuditBehaviorType;
-import org.labkey.api.gwt.client.model.GWTPropertyDescriptor;
-import org.labkey.api.module.ModuleHtmlView;
-import org.labkey.api.module.ModuleLoader;
-import org.labkey.api.pipeline.RecordedAction;
-import org.labkey.api.query.AbstractQueryImportAction;
-import org.labkey.api.query.AbstractQueryUpdateService;
-import org.labkey.api.query.BatchValidationException;
-import org.labkey.api.query.CustomView;
-import org.labkey.api.query.DefaultSchema;
-import org.labkey.api.query.DetailsURL;
-import org.labkey.api.query.DuplicateKeyException;
-import org.labkey.api.query.ExportScriptModel;
-import org.labkey.api.query.FieldKey;
-import org.labkey.api.query.FilteredTable;
-import org.labkey.api.query.InvalidKeyException;
-import org.labkey.api.query.MetadataUnavailableException;
-import org.labkey.api.query.QueryAction;
-import org.labkey.api.query.QueryDefinition;
-import org.labkey.api.query.QueryException;
-import org.labkey.api.query.QueryForm;
-import org.labkey.api.query.QueryParam;
-import org.labkey.api.query.QueryParseException;
-import org.labkey.api.query.QuerySchema;
-import org.labkey.api.query.QueryService;
-import org.labkey.api.query.QuerySettings;
-import org.labkey.api.query.QueryUpdateForm;
-import org.labkey.api.query.QueryUpdateService;
-import org.labkey.api.query.QueryUpdateServiceException;
-import org.labkey.api.query.QueryUrls;
-import org.labkey.api.query.QueryView;
-import org.labkey.api.query.RuntimeValidationException;
-import org.labkey.api.query.SchemaKey;
-import org.labkey.api.query.SimpleSchemaTreeVisitor;
-import org.labkey.api.query.TempQuerySettings;
-import org.labkey.api.query.UserSchema;
-import org.labkey.api.query.UserSchemaAction;
-import org.labkey.api.query.ValidationException;
-import org.labkey.api.reports.report.ReportDescriptor;
-import org.labkey.api.security.ActionNames;
-import org.labkey.api.security.AdminConsoleAction;
-import org.labkey.api.security.CSRF;
-import org.labkey.api.security.IgnoresTermsOfUse;
-import org.labkey.api.security.MutableSecurityPolicy;
-import org.labkey.api.security.RequiresAllOf;
-import org.labkey.api.security.RequiresAnyOf;
-import org.labkey.api.security.RequiresNoPermission;
-import org.labkey.api.security.RequiresPermission;
-import org.labkey.api.security.SecurityManager;
-import org.labkey.api.security.SecurityPolicyManager;
-import org.labkey.api.security.User;
-import org.labkey.api.security.UserManager;
-import org.labkey.api.security.ValidEmail;
-import org.labkey.api.security.permissions.AbstractActionPermissionTest;
-import org.labkey.api.security.permissions.AdminOperationsPermission;
-import org.labkey.api.security.permissions.AdminPermission;
-import org.labkey.api.security.permissions.DeletePermission;
-import org.labkey.api.security.permissions.EditSharedViewPermission;
-import org.labkey.api.security.permissions.InsertPermission;
-import org.labkey.api.security.permissions.MoveEntitiesPermission;
-import org.labkey.api.security.permissions.Permission;
-import org.labkey.api.security.permissions.PlatformDeveloperPermission;
-import org.labkey.api.security.permissions.ReadPermission;
-import org.labkey.api.security.permissions.UpdatePermission;
-import org.labkey.api.security.roles.EditorRole;
-import org.labkey.api.settings.AdminConsole;
-import org.labkey.api.settings.AppProps;
-import org.labkey.api.settings.LookAndFeelProperties;
-import org.labkey.api.stats.BaseAggregatesAnalyticsProvider;
-import org.labkey.api.stats.ColumnAnalyticsProvider;
-import org.labkey.api.util.ButtonBuilder;
-import org.labkey.api.util.DOM;
-import org.labkey.api.util.ExceptionUtil;
-import org.labkey.api.util.FileUtil;
-import org.labkey.api.util.HtmlString;
-import org.labkey.api.util.JavaScriptFragment;
-import org.labkey.api.util.JsonUtil;
-import org.labkey.api.util.LinkBuilder;
-import org.labkey.api.util.PageFlowUtil;
-import org.labkey.api.util.Pair;
-import org.labkey.api.util.ResponseHelper;
-import org.labkey.api.util.ReturnURLString;
-import org.labkey.api.util.StringExpression;
-import org.labkey.api.util.TestContext;
-import org.labkey.api.util.URLHelper;
-import org.labkey.api.util.UnexpectedException;
-import org.labkey.api.util.XmlBeansUtil;
-import org.labkey.api.view.ActionURL;
-import org.labkey.api.view.DetailsView;
-import org.labkey.api.view.HtmlView;
-import org.labkey.api.view.HttpView;
-import org.labkey.api.view.InsertView;
-import org.labkey.api.view.JspView;
-import org.labkey.api.view.NavTree;
-import org.labkey.api.view.NotFoundException;
-import org.labkey.api.view.UnauthorizedException;
-import org.labkey.api.view.UpdateView;
-import org.labkey.api.view.VBox;
-import org.labkey.api.view.ViewContext;
-import org.labkey.api.view.ViewServlet;
-import org.labkey.api.view.WebPartView;
-import org.labkey.api.view.template.PageConfig;
-import org.labkey.api.writer.HtmlWriter;
-import org.labkey.api.writer.ZipFile;
-import org.labkey.data.xml.ColumnType;
-import org.labkey.data.xml.ImportTemplateType;
-import org.labkey.data.xml.TableType;
-import org.labkey.data.xml.TablesDocument;
-import org.labkey.data.xml.TablesType;
-import org.labkey.data.xml.externalSchema.TemplateSchemaType;
-import org.labkey.data.xml.queryCustomView.FilterType;
-import org.labkey.query.AutoGeneratedDetailsCustomView;
-import org.labkey.query.AutoGeneratedInsertCustomView;
-import org.labkey.query.AutoGeneratedUpdateCustomView;
-import org.labkey.query.CustomViewImpl;
-import org.labkey.query.CustomViewUtil;
-import org.labkey.query.EditQueriesPermission;
-import org.labkey.query.EditableCustomView;
-import org.labkey.query.LinkedTableInfo;
-import org.labkey.query.MetadataTableJSON;
-import org.labkey.query.ModuleCustomQueryDefinition;
-import org.labkey.query.ModuleCustomView;
-import org.labkey.query.QueryServiceImpl;
-import org.labkey.query.TableXML;
-import org.labkey.query.audit.QueryExportAuditProvider;
-import org.labkey.query.audit.QueryUpdateAuditProvider;
-import org.labkey.query.model.MetadataTableJSONMixin;
-import org.labkey.query.persist.AbstractExternalSchemaDef;
-import org.labkey.query.persist.CstmView;
-import org.labkey.query.persist.ExternalSchemaDef;
-import org.labkey.query.persist.ExternalSchemaDefCache;
-import org.labkey.query.persist.LinkedSchemaDef;
-import org.labkey.query.persist.QueryDef;
-import org.labkey.query.persist.QueryManager;
-import org.labkey.query.reports.ReportsController;
-import org.labkey.query.reports.getdata.DataRequest;
-import org.labkey.query.sql.QNode;
-import org.labkey.query.sql.Query;
-import org.labkey.query.sql.SqlParser;
-import org.labkey.query.xml.ApiTestsDocument;
-import org.labkey.query.xml.TestCaseType;
-import org.labkey.remoteapi.RemoteConnections;
-import org.labkey.remoteapi.SelectRowsStreamHack;
-import org.labkey.remoteapi.query.SelectRowsCommand;
-import org.labkey.vfs.FileLike;
-import org.springframework.beans.MutablePropertyValues;
-import org.springframework.beans.PropertyValue;
-import org.springframework.beans.PropertyValues;
-import org.springframework.dao.DataAccessException;
-import org.springframework.dao.DataIntegrityViolationException;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.validation.BindException;
-import org.springframework.validation.Errors;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.servlet.ModelAndView;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.nio.file.Path;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.apache.commons.lang3.StringUtils.isBlank;
-import static org.apache.commons.lang3.StringUtils.trimToEmpty;
-import static org.labkey.api.action.ApiJsonWriter.CONTENT_TYPE_JSON;
-import static org.labkey.api.assay.AssayFileWriter.ensureUploadDirectory;
-import static org.labkey.api.data.DbScope.NO_OP_TRANSACTION;
-import static org.labkey.api.query.AbstractQueryUpdateService.saveFile;
-import static org.labkey.api.util.DOM.BR;
-import static org.labkey.api.util.DOM.DIV;
-import static org.labkey.api.util.DOM.FONT;
-import static org.labkey.api.util.DOM.Renderable;
-import static org.labkey.api.util.DOM.TABLE;
-import static org.labkey.api.util.DOM.TD;
-import static org.labkey.api.util.DOM.TR;
-import static org.labkey.api.util.DOM.at;
-import static org.labkey.api.util.DOM.cl;
-import static org.labkey.query.MetadataTableJSON.getTableType;
-import static org.labkey.query.MetadataTableJSON.parseDocument;
-
-@SuppressWarnings("DefaultAnnotationParam")
-
-public class QueryController extends SpringActionController
-{
- private static final Logger LOG = LogManager.getLogger(QueryController.class);
- private static final String ROW_ATTACHMENT_INDEX_DELIM = "::";
-
- private static final Set RESERVED_VIEW_NAMES = CaseInsensitiveHashSet.of(
- "Default",
- AutoGeneratedDetailsCustomView.NAME,
- AutoGeneratedInsertCustomView.NAME,
- AutoGeneratedUpdateCustomView.NAME
- );
-
- private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(QueryController.class,
- ValidateQueryAction.class,
- ValidateQueriesAction.class,
- GetSchemaQueryTreeAction.class,
- GetQueryDetailsAction.class,
- ViewQuerySourceAction.class
- );
-
- public QueryController()
- {
- setActionResolver(_actionResolver);
- }
-
- public static void registerAdminConsoleLinks()
- {
- AdminConsole.addLink(AdminConsole.SettingsLinkType.Diagnostics, "data sources", new ActionURL(DataSourceAdminAction.class, ContainerManager.getRoot()));
- }
-
- public static class RemoteQueryConnectionUrls
- {
- public static ActionURL urlManageRemoteConnection(Container c)
- {
- return new ActionURL(ManageRemoteConnectionsAction.class, c);
- }
-
- public static ActionURL urlCreateRemoteConnection(Container c)
- {
- return new ActionURL(EditRemoteConnectionAction.class, c);
- }
-
- public static ActionURL urlEditRemoteConnection(Container c, String connectionName)
- {
- ActionURL url = new ActionURL(EditRemoteConnectionAction.class, c);
- url.addParameter("connectionName", connectionName);
- return url;
- }
-
- public static ActionURL urlSaveRemoteConnection(Container c)
- {
- return new ActionURL(EditRemoteConnectionAction.class, c);
- }
-
- public static ActionURL urlDeleteRemoteConnection(Container c, @Nullable String connectionName)
- {
- ActionURL url = new ActionURL(DeleteRemoteConnectionAction.class, c);
- if (connectionName != null)
- url.addParameter("connectionName", connectionName);
- return url;
- }
-
- public static ActionURL urlTestRemoteConnection(Container c, String connectionName)
- {
- ActionURL url = new ActionURL(TestRemoteConnectionAction.class, c);
- url.addParameter("connectionName", connectionName);
- return url;
- }
- }
-
- @RequiresPermission(AdminOperationsPermission.class)
- public static class EditRemoteConnectionAction extends FormViewAction
- {
- @Override
- public void validateCommand(RemoteConnections.RemoteConnectionForm target, Errors errors)
- {
- }
-
- @Override
- public ModelAndView getView(RemoteConnections.RemoteConnectionForm remoteConnectionForm, boolean reshow, BindException errors)
- {
- remoteConnectionForm.setConnectionKind(RemoteConnections.CONNECTION_KIND_QUERY);
- if (!errors.hasErrors())
- {
- String name = remoteConnectionForm.getConnectionName();
- // package the remote-connection properties into the remoteConnectionForm and pass them along
- Map map1 = RemoteConnections.getRemoteConnection(RemoteConnections.REMOTE_QUERY_CONNECTIONS_CATEGORY, name, getContainer());
- remoteConnectionForm.setUrl(map1.get("URL"));
- remoteConnectionForm.setUserEmail(map1.get("user"));
- remoteConnectionForm.setPassword(map1.get("password"));
- remoteConnectionForm.setFolderPath(map1.get("container"));
- }
- setHelpTopic("remoteConnection");
- return new JspView<>("/org/labkey/query/view/createRemoteConnection.jsp", remoteConnectionForm, errors);
- }
-
- @Override
- public boolean handlePost(RemoteConnections.RemoteConnectionForm remoteConnectionForm, BindException errors)
- {
- return RemoteConnections.createOrEditRemoteConnection(remoteConnectionForm, getContainer(), errors);
- }
-
- @Override
- public URLHelper getSuccessURL(RemoteConnections.RemoteConnectionForm remoteConnectionForm)
- {
- return RemoteQueryConnectionUrls.urlManageRemoteConnection(getContainer());
- }
-
- @Override
- public void addNavTrail(NavTree root)
- {
- new BeginAction(getViewContext()).addNavTrail(root);
- root.addChild("Create/Edit Remote Connection", new QueryUrlsImpl().urlExternalSchemaAdmin(getContainer()));
- }
- }
-
- @RequiresPermission(AdminOperationsPermission.class)
- public static class DeleteRemoteConnectionAction extends FormViewAction
- {
- @Override
- public void validateCommand(RemoteConnections.RemoteConnectionForm target, Errors errors)
- {
- }
-
- @Override
- public ModelAndView getView(RemoteConnections.RemoteConnectionForm remoteConnectionForm, boolean reshow, BindException errors)
- {
- return new JspView<>("/org/labkey/query/view/confirmDeleteConnection.jsp", remoteConnectionForm, errors);
- }
-
- @Override
- public boolean handlePost(RemoteConnections.RemoteConnectionForm remoteConnectionForm, BindException errors)
- {
- remoteConnectionForm.setConnectionKind(RemoteConnections.CONNECTION_KIND_QUERY);
- return RemoteConnections.deleteRemoteConnection(remoteConnectionForm, getContainer());
- }
-
- @Override
- public URLHelper getSuccessURL(RemoteConnections.RemoteConnectionForm remoteConnectionForm)
- {
- return RemoteQueryConnectionUrls.urlManageRemoteConnection(getContainer());
- }
-
- @Override
- public void addNavTrail(NavTree root)
- {
- new BeginAction(getViewContext()).addNavTrail(root);
- root.addChild("Confirm Delete Connection", new QueryUrlsImpl().urlExternalSchemaAdmin(getContainer()));
- }
- }
-
- @RequiresPermission(AdminOperationsPermission.class)
- public static class TestRemoteConnectionAction extends FormViewAction
- {
- @Override
- public void validateCommand(RemoteConnections.RemoteConnectionForm target, Errors errors)
- {
- }
-
- @Override
- public ModelAndView getView(RemoteConnections.RemoteConnectionForm remoteConnectionForm, boolean reshow, BindException errors)
- {
- String name = remoteConnectionForm.getConnectionName();
- String schemaName = "core"; // test Schema Name
- String queryName = "Users"; // test Query Name
-
- // Extract the username, password, and container from the secure property store
- Map singleConnectionMap = RemoteConnections.getRemoteConnection(RemoteConnections.REMOTE_QUERY_CONNECTIONS_CATEGORY, name, getContainer());
- if (singleConnectionMap.isEmpty())
- throw new NotFoundException();
- String url = singleConnectionMap.get(RemoteConnections.FIELD_URL);
- String user = singleConnectionMap.get(RemoteConnections.FIELD_USER);
- String password = singleConnectionMap.get(RemoteConnections.FIELD_PASSWORD);
- String container = singleConnectionMap.get(RemoteConnections.FIELD_CONTAINER);
-
- // connect to the remote server and retrieve an input stream
- org.labkey.remoteapi.Connection cn = new org.labkey.remoteapi.Connection(url, user, password);
- final SelectRowsCommand cmd = new SelectRowsCommand(schemaName, queryName);
- try
- {
- DataIteratorBuilder source = SelectRowsStreamHack.go(cn, container, cmd, getContainer());
- // immediately close the source after opening it, this is a test.
- source.getDataIterator(new DataIteratorContext()).close();
- }
- catch (Exception e)
- {
- errors.addError(new LabKeyError("The listed credentials for this remote connection failed to connect."));
- return new JspView<>("/org/labkey/query/view/testRemoteConnectionsFailure.jsp", remoteConnectionForm);
- }
-
- return new JspView<>("/org/labkey/query/view/testRemoteConnectionsSuccess.jsp", remoteConnectionForm);
- }
-
- @Override
- public boolean handlePost(RemoteConnections.RemoteConnectionForm remoteConnectionForm, BindException errors)
- {
- return true;
- }
-
- @Override
- public URLHelper getSuccessURL(RemoteConnections.RemoteConnectionForm remoteConnectionForm)
- {
- return null;
- }
-
- @Override
- public void addNavTrail(NavTree root)
- {
- new BeginAction(getViewContext()).addNavTrail(root);
- root.addChild("Manage Remote Connections", new QueryUrlsImpl().urlExternalSchemaAdmin(getContainer()));
- }
- }
-
- public static class QueryUrlsImpl implements QueryUrls
- {
- @Override
- public ActionURL urlSchemaBrowser(Container c)
- {
- return new ActionURL(BeginAction.class, c);
- }
-
- @Override
- public ActionURL urlSchemaBrowser(Container c, @Nullable String schemaName)
- {
- ActionURL ret = urlSchemaBrowser(c);
- if (schemaName != null)
- {
- ret.addParameter(QueryParam.schemaName.toString(), schemaName);
- }
- return ret;
- }
-
- @Override
- public ActionURL urlSchemaBrowser(Container c, @Nullable String schemaName, @Nullable String queryName)
- {
- if (StringUtils.isEmpty(queryName))
- return urlSchemaBrowser(c, schemaName);
- ActionURL ret = urlSchemaBrowser(c);
- ret.addParameter(QueryParam.schemaName.toString(), trimToEmpty(schemaName));
- ret.addParameter(QueryParam.queryName.toString(), trimToEmpty(queryName));
- return ret;
- }
-
- public ActionURL urlExternalSchemaAdmin(Container c)
- {
- return urlExternalSchemaAdmin(c, null);
- }
-
- public ActionURL urlExternalSchemaAdmin(Container c, @Nullable String message)
- {
- ActionURL url = new ActionURL(AdminAction.class, c);
-
- if (null != message)
- url.addParameter("message", message);
-
- return url;
- }
-
- public ActionURL urlInsertExternalSchema(Container c)
- {
- return new ActionURL(InsertExternalSchemaAction.class, c);
- }
-
- public ActionURL urlNewQuery(Container c)
- {
- return new ActionURL(NewQueryAction.class, c);
- }
-
- public ActionURL urlUpdateExternalSchema(Container c, AbstractExternalSchemaDef def)
- {
- ActionURL url = new ActionURL(EditExternalSchemaAction.class, c);
- url.addParameter("externalSchemaId", Integer.toString(def.getExternalSchemaId()));
- return url;
- }
-
- public ActionURL urlReloadExternalSchema(Container c, AbstractExternalSchemaDef def)
- {
- ActionURL url = new ActionURL(ReloadExternalSchemaAction.class, c);
- url.addParameter("externalSchemaId", Integer.toString(def.getExternalSchemaId()));
- return url;
- }
-
- public ActionURL urlDeleteSchema(Container c, AbstractExternalSchemaDef def)
- {
- ActionURL url = new ActionURL(DeleteSchemaAction.class, c);
- url.addParameter("externalSchemaId", Integer.toString(def.getExternalSchemaId()));
- return url;
- }
-
- @Override
- public ActionURL urlStartBackgroundRReport(@NotNull ActionURL baseURL, String reportId)
- {
- ActionURL result = baseURL.clone();
- result.setAction(ReportsController.StartBackgroundRReportAction.class);
- result.replaceParameter(ReportDescriptor.Prop.reportId, reportId);
- return result;
- }
-
- @Override
- public ActionURL urlExecuteQuery(@NotNull ActionURL baseURL)
- {
- ActionURL result = baseURL.clone();
- result.setAction(ExecuteQueryAction.class);
- return result;
- }
-
- @Override
- public ActionURL urlExecuteQuery(Container c, String schemaName, String queryName)
- {
- return new ActionURL(ExecuteQueryAction.class, c)
- .addParameter(QueryParam.schemaName, schemaName)
- .addParameter(QueryParam.queryName, queryName);
- }
-
- @Override
- public @NotNull ActionURL urlCreateExcelTemplate(Container c, String schemaName, String queryName)
- {
- return new ActionURL(ExportExcelTemplateAction.class, c)
- .addParameter(QueryParam.schemaName, schemaName)
- .addParameter("query.queryName", queryName);
- }
-
- @Override
- public ActionURL urlMetadataQuery(Container c, String schemaName, String queryName)
- {
- return new ActionURL(MetadataQueryAction.class, c)
- .addParameter(QueryParam.schemaName, schemaName)
- .addParameter(QueryParam.queryName, queryName);
- }
- }
-
- @Override
- public PageConfig defaultPageConfig()
- {
- // set default help topic for query controller
- PageConfig config = super.defaultPageConfig();
- config.setHelpTopic("querySchemaBrowser");
- return config;
- }
-
- @AdminConsoleAction(AdminOperationsPermission.class)
- public static class DataSourceAdminAction extends SimpleViewAction