diff --git a/app/models/reader_preferences.rb b/app/models/reader_preferences.rb
new file mode 100644
index 00000000000..82df50ddfd2
--- /dev/null
+++ b/app/models/reader_preferences.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+class ReaderPreferences
+
+ def self.get(preference)
+ preference_name = preference.to_s.upcase
+ value = client.get(preference_name) || ENV[preference_name]
+
+ if value
+ return value.to_i
+ end
+
+ "#{preference_name} is not in the list of allowed preferences.. #{valid_preferences}"
+ end
+
+ def self.set(preference, new_value)
+ preference_name = preference.to_s.upcase
+
+ current_value = client.get(preference_name) || ENV[preference_name]
+
+ if current_value
+ client.set(preference_name, new_value.to_s)
+ "#{preference_name} set to #{new_value}"
+ else
+ "#{preference_name} is not in the list of allowed preferences.. #{valid_preferences}"
+ end
+ end
+
+ def self.delete(preference)
+ preference_name = preference.to_s.upcase
+
+ current_value = client.get(preference_name)
+
+ if current_value
+ client.del preference_name
+
+ "#{preference_name} was reset to default value #{ENV[preference_name]}"
+ else
+ "#{preference_name} is not set to a custom value"
+ end
+ end
+
+ def self.client
+ # Use separate Redis namespace for test to avoid conflicts between test and dev environments
+ @cache_namespace ||= Rails.env.test? ? :reader_preferences_test : :reader_preferences
+ @client ||= Redis::Namespace.new(@cache_namespace, redis: redis)
+ end
+
+ def self.redis
+ @redis ||= Redis.new(url: Rails.application.secrets.redis_url_cache)
+ end
+
+ # Default values are stored as ENV enviroment dependent values in the file
+ # appeals-deployment/ansible/roles/caseflow-certification/defaults/main.yml
+ # Example:
+ # http_env_specific_environment:
+ # prod:
+ # READER_DELAY_BEFORE_PROGRESS_BAR: 1000
+ # READER_SHOW_PROGRESS_BAR_THRESHOLD: 3000
+
+ # prodtest:
+ # READER_DELAY_BEFORE_PROGRESS_BAR: 1000
+ # READER_SHOW_PROGRESS_BAR_THRESHOLD: 3000
+
+ # preprod:
+ # READER_DELAY_BEFORE_PROGRESS_BAR: 1000
+ # READER_SHOW_PROGRESS_BAR_THRESHOLD: 3000
+
+ # uat:
+ # READER_DELAY_BEFORE_PROGRESS_BAR: 1000
+ # READER_SHOW_PROGRESS_BAR_THRESHOLD: 3000
+
+ def self.bandwidth_warning_enabled?
+ max_file_wait_time = ENV.fetch("MAX_FILE_WAIT_TIME", nil)
+ max_file_wait_time.present? && max_file_wait_time.to_i > 0
+ end
+
+ def self.valid_preferences
+ @valid_preferences ||= ENV.select { |feature, value| feature.include?("READER") }.keys
+ end
+end
diff --git a/app/views/reader/appeal/index.html.erb b/app/views/reader/appeal/index.html.erb
index e413c343c03..0f3a2c60c56 100644
--- a/app/views/reader/appeal/index.html.erb
+++ b/app/views/reader/appeal/index.html.erb
@@ -12,6 +12,7 @@
featureToggles: {
interfaceVersion2: FeatureToggle.enabled?(:interface_version_2, user: current_user),
bandwidthBanner: FeatureToggle.enabled?(:bandwidth_banner, user: current_user),
+ warningIconAndBanner: FeatureToggle.enabled?(:warning_icon_and_banner, user: current_user),
windowSlider: FeatureToggle.enabled?(:window_slider, user: current_user),
readerSelectorsMemoized: FeatureToggle.enabled?(:bulk_upload_documents, user: current_user),
readerGetDocumentLogging: FeatureToggle.enabled?(:reader_get_document_logging, user: current_user),
@@ -25,7 +26,13 @@
pdfPageRenderTimeInMs: FeatureToggle.enabled?(:pdf_page_render_time_in_ms, user: current_user),
prefetchDisabled: FeatureToggle.enabled?(:prefetch_disabled, user: current_user),
readerSearchImprovements: FeatureToggle.enabled?(:reader_search_improvements, user: current_user),
- readerPrototype: FeatureToggle.enabled?(:reader_prototype, user: current_user)
+ readerPrototype: FeatureToggle.enabled?(:reader_prototype, user: current_user),
+ readerProgressBar: FeatureToggle.enabled?(:reader_progress_bar, user: current_user),
+ },
+ readerPreferences: {
+ delayBeforeProgressBar: ReaderPreferences.get(:delay_before_progress_bar),
+ showProgressBarThreshold: ReaderPreferences.get(:show_progress_bar_threshold),
+ maximumFileWaitTime: ReaderPreferences.get(:maximum_file_wait_time),
},
buildDate: build_date
}) %>
diff --git a/client/app/components/common/actions.js b/client/app/components/common/actions.js
index d0c1f673cfd..8803a2b5a1e 100644
--- a/client/app/components/common/actions.js
+++ b/client/app/components/common/actions.js
@@ -121,6 +121,13 @@ export const setScheduledHearing = (payload) => ({
payload,
});
+export const setFeatureToggles = (toggles) => ({
+ type: ACTIONS.SET_FEATURE_TOGGLES,
+ payload: {
+ toggles
+ },
+});
+
export const fetchScheduledHearings = (hearingDay) => (dispatch) => {
// Dispatch the action to set the pending state for the hearing time
dispatch({ type: ACTIONS.REQUEST_SCHEDULED_HEARINGS });
diff --git a/client/app/components/common/reducers.js b/client/app/components/common/reducers.js
index 5337604207a..4e64d56284e 100644
--- a/client/app/components/common/reducers.js
+++ b/client/app/components/common/reducers.js
@@ -4,6 +4,7 @@ import { update } from '../../util/ReducerUtil';
export const initialState = {
scheduledHearingsList: [],
fetchingHearings: false,
+ isWarningIconAndBannerEnabled: false,
dropdowns: {
judges: {},
hearingCoordinators: {},
@@ -188,4 +189,15 @@ const commonComponentsReducer = (state = initialState, action = {}) => {
}
};
+const featureTogglesReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case ACTIONS.SET_FEATURE_TOGGLES:
+ return {
+ ...state,
+ ...action.payload,
+ };
+ default:
+ return state;
+ }
+};
export default commonComponentsReducer;
diff --git a/client/app/components/icons/SizeWarningIcon.jsx b/client/app/components/icons/SizeWarningIcon.jsx
new file mode 100644
index 00000000000..fa2d9ba87a0
--- /dev/null
+++ b/client/app/components/icons/SizeWarningIcon.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ICON_SIZES } from '../../constants/AppConstants';
+
+export const SizeWarningIcon = (props) => {
+ const { size, className, isWarningIconAndBannerEnabled } = props;
+
+ if (!isWarningIconAndBannerEnabled) {
+ return null;
+ }
+
+ return (
+ );
+};
+
+
+SizeWarningIcon.propTypes = {
+
+ /**
+ Sets height of the component, width is set automatically by the svg viewbox property.
+ Default height is 'ICON_SIZES.MEDIUM'.
+ */
+ size: PropTypes.number,
+
+ /**
+ Adds class to the component. Default value is ''.
+ */
+ className: PropTypes.string,
+
+ /**
+ Enables or disables the Size Warning Icon based on the feature toggle.
+ */
+ isWarningIconAndBannerEnabled: PropTypes.bool.isRequired
+};
+
+SizeWarningIcon.defaultProps = {
+ size: ICON_SIZES.MEDIUM,
+ className: ''
+};
diff --git a/client/app/reader/BandwidthAlert.jsx b/client/app/reader/BandwidthAlert.jsx
index 67939c00640..4f45c226950 100644
--- a/client/app/reader/BandwidthAlert.jsx
+++ b/client/app/reader/BandwidthAlert.jsx
@@ -3,6 +3,7 @@ import Alert from '../components/Alert';
import { css } from 'glamor';
import { storeMetrics } from '../util/Metrics';
import uuid from 'uuid';
+import PropTypes from 'prop-types';
// variables being defined are in mbps
const bandwidthThreshold = 1.5;
@@ -20,7 +21,7 @@ class BandwidthAlert extends React.Component {
}
componentDidMount() {
- if ('connection' in navigator) {
+ if (this.props.isWarningIconAndBannerEnabled && 'connection' in navigator) {
this.updateConnectionInfo();
}
}
@@ -42,6 +43,11 @@ class BandwidthAlert extends React.Component {
};
render() {
+ const { isWarningIconAndBannerEnabled } = this.props;
+
+ if (!isWarningIconAndBannerEnabled) {
+ return null;
+ }
if (this.state.displayBandwidthAlert) {
return (
@@ -59,4 +65,8 @@ class BandwidthAlert extends React.Component {
}
}
+BandwidthAlert.propTypes = {
+ isWarningIconAndBannerEnabled: PropTypes.bool.isRequired
+};
+
export default BandwidthAlert;
diff --git a/client/app/reader/PdfListView.jsx b/client/app/reader/PdfListView.jsx
index 4dbe9d3a6cd..da88b3d2470 100644
--- a/client/app/reader/PdfListView.jsx
+++ b/client/app/reader/PdfListView.jsx
@@ -18,108 +18,81 @@ import NoSearchResults from './NoSearchResults';
import { fetchAppealDetails, onReceiveAppealDetails } from '../reader/PdfViewer/PdfViewerActions';
import { shouldFetchAppeal } from '../reader/utils';
import { DOCUMENTS_OR_COMMENTS_ENUM } from './DocumentList/actionTypes';
+import { SizeWarningIcon } from '../components/icons/SizeWarningIcon';
export class PdfListView extends React.Component {
- setClearAllFiltersCallbacks = (callbacks) => {
- this.setState({ clearAllFiltersCallbacks: [...this.state.clearAllFiltersCallbacks, ...callbacks] });
- };
-
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.state = {
- clearAllFiltersCallbacks: []
+ clearAllFiltersCallbacks: [],
};
}
+ setClearAllFiltersCallbacks = (callbacks) => {
+ this.setState((prevState) => ({
+ clearAllFiltersCallbacks: [...prevState.clearAllFiltersCallbacks, ...callbacks],
+ }));
+ };
+
componentDidMount() {
- if (shouldFetchAppeal(this.props.appeal, this.props.match.params.vacolsId)) {
- // if the appeal is fetched through case selected appeals, re-use that existing appeal
- // information.
- if (this.props.caseSelectedAppeal &&
- (this.props.caseSelectedAppeal.vacols_id === this.props.match.params.vacolsId)) {
- this.props.onReceiveAppealDetails(this.props.caseSelectedAppeal);
+ const { appeal, match, caseSelectedAppeal, fetchAppealDetails, onReceiveAppealDetails } = this.props;
+
+ if (shouldFetchAppeal(appeal, match.params.vacolsId)) {
+ if (caseSelectedAppeal && caseSelectedAppeal.vacols_id === match.params.vacolsId) {
+ onReceiveAppealDetails(caseSelectedAppeal);
} else {
- this.props.fetchAppealDetails(this.props.match.params.vacolsId);
+ fetchAppealDetails(match.params.vacolsId);
}
-
- // if appeal is loaded from the assignments and it matches the vacols id
- // in the url
- } else if (this.props.appeal.vacols_id === this.props.match.params.vacolsId) {
- this.props.onReceiveAppealDetails(this.props.appeal);
+ } else if (appeal?.vacols_id === match.params.vacolsId) {
+ onReceiveAppealDetails(appeal);
}
}
render() {
- const noDocuments = !_.size(this.props.documents) && _.size(this.props.docFilterCriteria.searchQuery) > 0;
- let tableView;
+ const { documents, docFilterCriteria, viewingDocumentsOrComments, featureToggles } = this.props;
+ const noDocuments = !_.size(documents) && _.size(docFilterCriteria?.searchQuery) > 0;
+ let tableView;
if (noDocuments) {
tableView =