diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 00876e8100e..4691f672563 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -251,11 +251,15 @@ def contested_claim? category_substrings = %w[Contested Apportionment] - request_issues.active.any? do |request_issue| - category_substrings.any? do |substring| - request_issues.active.include?(request_issue) && request_issue.nonrating_issue_category&.include?(substring) + request_issues.each do |request_issue| + category_substrings.each do |substring| + if request_issue.active? && request_issue.nonrating_issue_category&.include?(substring) + return true + end end end + + false end # :reek:RepeatedConditionals diff --git a/app/models/decision_review.rb b/app/models/decision_review.rb index 7e1306187f4..6ecd2e07278 100644 --- a/app/models/decision_review.rb +++ b/app/models/decision_review.rb @@ -125,7 +125,11 @@ def veteran_full_name end def number_of_issues - request_issues.active.count + request_issues.count(&:active?) + end + + def issue_categories + request_issues.select(&:active?).map(&:nonrating_issue_category) end def external_id diff --git a/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb b/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb index 92fb7acc108..0b7924744d9 100644 --- a/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb +++ b/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb @@ -21,13 +21,31 @@ def tasks assigned_tasks end + # Override task_includes to optimize the queue a bit + def task_includes + [ + { appeal: [ + :request_issues, + :available_hearing_locations, + :claimants, + :work_mode, + :latest_informal_hearing_presentation_task + ] }, + :assigned_by, + :assigned_to, + :children, + :parent, + :attorney_case_reviews + ] + end + def column_names SpecialtyCaseTeam::COLUMN_NAMES end # This only affects bulk assign on the standard queue tab view def allow_bulk_assign? - true + false end def hide_from_queue_table_view @@ -37,8 +55,4 @@ def hide_from_queue_table_view def no_task_limit false end - - def custom_task_limit - 60 - end end diff --git a/app/models/serializers/work_queue/task_column_serializer.rb b/app/models/serializers/work_queue/task_column_serializer.rb index e1ad582a7e2..da5cc6bee81 100644 --- a/app/models/serializers/work_queue/task_column_serializer.rb +++ b/app/models/serializers/work_queue/task_column_serializer.rb @@ -90,11 +90,7 @@ def self.serialize_attribute?(params, columns) columns = [Constants.QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name] if serialize_attribute?(params, columns) - if object.appeal.is_a?(LegacyAppeal) - object.appeal.issue_categories - else - object.appeal.request_issues.active.map(&:nonrating_issue_category) - end.join(",") + object.appeal.issue_categories.join(",") end end diff --git a/app/models/serializers/work_queue/task_serializer.rb b/app/models/serializers/work_queue/task_serializer.rb index 4af2adfddb8..7bb06311269 100644 --- a/app/models/serializers/work_queue/task_serializer.rb +++ b/app/models/serializers/work_queue/task_serializer.rb @@ -146,11 +146,7 @@ class WorkQueue::TaskSerializer end attribute :issue_types do |object| - if object.appeal.is_a?(LegacyAppeal) - object.appeal.issue_categories - else - object.appeal.request_issues.active.map(&:nonrating_issue_category) - end.join(",") + object.appeal.issue_categories.join(",") end attribute :external_hearing_id do |object| diff --git a/client/app/queue/QueueActions.js b/client/app/queue/QueueActions.js index f5fb69d3dec..d4cc8ddf1d2 100644 --- a/client/app/queue/QueueActions.js +++ b/client/app/queue/QueueActions.js @@ -14,6 +14,7 @@ import ApiUtil from '../util/ApiUtil'; import { getMinutesToMilliseconds } from '../util/DateUtil'; import pluralize from 'pluralize'; import { keyBy, pick } from 'lodash'; +import { removeTaskIdsFromCache } from './caching/queueTableCache.slice'; export const onReceiveQueue = ( { tasks, amaTasks, appeals } @@ -419,12 +420,13 @@ export const fetchTasksAndAppealsOfAttorney = (attorneyId, params) => (dispatch) }; export const setSelectionOfTaskOfUser = - ({ userId, taskId, selected }) => ({ + ({ userId, taskId, selected, task }) => ({ type: ACTIONS.SET_SELECTION_OF_TASK_OF_USER, payload: { userId, taskId, - selected + selected, + task } }); @@ -507,6 +509,9 @@ export const initialSpecialtyCaseTeamAssignTasksToUser = ({ then((resp) => { const receievedTasks = prepareAllTasksForStore(resp.tasks.data); + // Removes tasks from queue table cache if using redux caching instead of local component state + dispatch(removeTaskIdsFromCache({ taskIds })); + dispatch(onReceiveTasks(pick(receievedTasks, ['tasks', 'amaTasks']))); dispatch(incrementTaskCountForAttorney({ diff --git a/client/app/queue/QueueTable.jsx b/client/app/queue/QueueTable.jsx index 4267a27a259..42d4098bf55 100644 --- a/client/app/queue/QueueTable.jsx +++ b/client/app/queue/QueueTable.jsx @@ -353,10 +353,21 @@ export default class QueueTable extends React.PureComponent { if (this.props.rowObjects.length) { this.setState({ cachedResponses: { ...this.state.cachedResponses, [this.requestUrl()]: firstResponse } }); + + if (this.props.useReduxCache) { + this.props.updateReduxCache({ key: this.requestUrl(), value: firstResponse }); + } + } }; componentDidUpdate = (previousProps, previousState) => { + if (this.props.useReduxCache && + (this.props.reduxCache[this.requestUrl()]?.tasks?.length !== + previousProps.reduxCache[this.requestUrl()]?.tasks?.length)) { + this.setState({ tasksFromApi: this.props.reduxCache[this.requestUrl()].tasks }); + } + // Only refetch if the search query text changes if (this.props.tabPaginationOptions && previousState.querySearchText !== this.props.tabPaginationOptions[QUEUE_CONFIG.SEARCH_QUERY_REQUEST_PARAM]) { @@ -556,9 +567,29 @@ export default class QueueTable extends React.PureComponent { deepLink = () => { const base = `${window.location.origin}${window.location.pathname}`; - const tab = this.props.taskPagesApiEndpoint.split('?')[1]; + const currentParams = new URLSearchParams(window.location.search); + const tableParams = new URLSearchParams(this.requestQueryString()); + const tabParams = new URLSearchParams(this.props.taskPagesApiEndpoint.split('?')[1]); + + // List of parameters that should be cleared if not present in tableParams + const paramsToClear = [ + QUEUE_CONFIG.SEARCH_QUERY_REQUEST_PARAM, + `${QUEUE_CONFIG.FILTER_COLUMN_REQUEST_PARAM}[]`, + ]; + + // Remove paramsToClear from currentParams if they are not in tableParams + paramsToClear.forEach((param) => { + if (!tableParams.has(param)) { + currentParams.delete(param); + } + }); + + // Merge tableParams and tabParams into currentParams, overwriting any duplicate keys + for (const [key, value] of [...tabParams.entries(), ...tableParams.entries()]) { + currentParams.set(key, value); + } - return `${base}?${tab}${this.requestQueryString()}`; + return `${base}?${currentParams.toString()}`; }; // /organizations/vlj-support-staff/tasks?tab=on_hold @@ -622,7 +653,8 @@ export default class QueueTable extends React.PureComponent { const endpointUrl = this.requestUrl(); // If we already have the tasks cached then we set the state and return early. - const responseFromCache = this.state.cachedResponses[endpointUrl]; + const responseFromCache = this.props.useReduxCache ? this.props.reduxCache[endpointUrl] : + this.state.cachedResponses[endpointUrl]; if (responseFromCache) { this.setState({ tasksFromApi: responseFromCache.tasks }); @@ -643,11 +675,21 @@ export default class QueueTable extends React.PureComponent { const preparedResponse = Object.assign(response.body, { tasks: preparedTasks }); this.setState({ - cachedResponses: { ...this.state.cachedResponses, [endpointUrl]: preparedResponse }, + // cachedResponses: { ...this.state.cachedResponses, [endpointUrl]: preparedResponse }, + ...(!this.props.useReduxCache && { + cachedResponses: { + ...this.state.cachedResponses, + [endpointUrl]: preparedResponse + } + }), tasksFromApi: preparedTasks, loadingComponent: null }); + if (this.props.useReduxCache) { + this.props.updateReduxCache({ key: endpointUrl, value: preparedResponse }); + } + this.updateAddressBar(); }). catch(() => this.setState({ loadingComponent: null })); @@ -669,7 +711,9 @@ export default class QueueTable extends React.PureComponent { styling, bodyStyling, enablePagination, - useTaskPagesApi + useTaskPagesApi, + reduxCache, + useReduxCache } = this.props; let { totalTaskCount, numberOfPages, rowObjects, casesPerPage } = this.props; @@ -682,7 +726,7 @@ export default class QueueTable extends React.PureComponent { // If we already have the response cached then use the attributes of the response to set the pagination vars. const endpointUrl = this.requestUrl(); - const responseFromCache = this.state.cachedResponses[endpointUrl]; + const responseFromCache = useReduxCache ? reduxCache[endpointUrl] : this.state.cachedResponses[endpointUrl]; if (responseFromCache) { numberOfPages = responseFromCache.task_page_count; @@ -843,6 +887,9 @@ HeaderRow.propTypes = FooterRow.propTypes = Row.propTypes = BodyRows.propTypes = }), onHistoryUpdate: PropTypes.func, preserveFilter: PropTypes.bool, + useReduxCache: PropTypes.bool, + reduxCache: PropTypes.object, + updateReduxCache: PropTypes.func }; Row.propTypes.rowObjects = PropTypes.arrayOf(PropTypes.object); diff --git a/client/app/queue/QueueTableBuilder.jsx b/client/app/queue/QueueTableBuilder.jsx index 0de35805a32..15d1077b12d 100644 --- a/client/app/queue/QueueTableBuilder.jsx +++ b/client/app/queue/QueueTableBuilder.jsx @@ -10,38 +10,12 @@ import QueueTable from './QueueTable'; import TabWindow from '../components/TabWindow'; import Link from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/Link'; import QueueOrganizationDropdown from './components/QueueOrganizationDropdown'; -import { - assignedToColumn, - assignedByColumn, - badgesColumn, - boardIntakeColumn, - completedToNameColumn, - daysOnHoldColumn, - daysSinceLastActionColumn, - daysSinceIntakeColumn, - receiptDateColumn, - daysWaitingColumn, - detailsColumn, - docketNumberColumn, - documentIdColumn, - lastActionColumn, - issueCountColumn, - issueTypesColumn, - readerLinkColumn, - readerLinkColumnWithNewDocsIcon, - regionalOfficeColumn, - taskColumn, - taskOwnerColumn, - taskCompletedDateColumn, - typeColumn, - vamcOwnerColumn -} from './components/TaskTableColumns'; import { tasksWithAppealsFromRawTasks } from './utils'; import COPY from '../../COPY'; -import QUEUE_CONFIG from '../../constants/QUEUE_CONFIG'; import { css } from 'glamor'; import { isActiveOrganizationVHA } from '../queue/selectors'; +import { columnsFromConfig } from './queueTableUtils'; const rootStyles = css({ '.usa-alert + &': { @@ -86,94 +60,6 @@ const QueueTableBuilder = (props) => { return config; }; - const filterValuesForColumn = (column) => - column && column.filterable && column.filter_options; - - const createColumnObject = (column, config, tasks) => { - - const { requireDasRecord } = props; - const filterOptions = filterValuesForColumn(column); - const functionForColumn = { - [QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name]: typeColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.BADGES.name]: badgesColumn(tasks), - [QUEUE_CONFIG.COLUMNS.CASE_DETAILS_LINK.name]: detailsColumn( - tasks, - requireDasRecord, - config.userRole - ), - [QUEUE_CONFIG.COLUMNS.DAYS_ON_HOLD.name]: daysOnHoldColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_LAST_ACTION.name]: daysSinceLastActionColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name]: daysWaitingColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DOCKET_NUMBER.name]: docketNumberColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DOCUMENT_COUNT_READER_LINK.name]: readerLinkColumn( - requireDasRecord, - true - ), - [QUEUE_CONFIG.COLUMNS.DOCUMENT_ID.name]: documentIdColumn(), - [QUEUE_CONFIG.COLUMNS.ISSUE_COUNT.name]: issueCountColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name]: issueTypesColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.READER_LINK_WITH_NEW_DOCS_ICON. - name]: readerLinkColumnWithNewDocsIcon(requireDasRecord), - [QUEUE_CONFIG.COLUMNS.REGIONAL_OFFICE.name]: regionalOfficeColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name]: assignedToColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.BOARD_INTAKE.name]: boardIntakeColumn( - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.LAST_ACTION.name]: lastActionColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_OWNER.name]: taskOwnerColumn( - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.VAMC_OWNER.name]: vamcOwnerColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNER.name]: completedToNameColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name]: assignedByColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name]: taskCompletedDateColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_TYPE.name]: taskColumn(tasks, filterOptions), - [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_INTAKE.name]: daysSinceIntakeColumn(requireDasRecord), - [QUEUE_CONFIG.COLUMNS.RECEIPT_DATE_INTAKE.name]: receiptDateColumn(), - }; - - return functionForColumn[column.name]; - }; - - const columnsFromConfig = (config, tabConfig, tasks) => - (tabConfig.columns || []).map((column) => - createColumnObject(column, config, tasks) - ); - const taskTableTabFactory = (tabConfig, config) => { const savedPaginationOptions = storedPaginationOptions; const tasks = tasksWithAppealsFromRawTasks(tabConfig.tasks); diff --git a/client/app/queue/UnassignedCasesPage.jsx b/client/app/queue/UnassignedCasesPage.jsx index 2c2230feee6..34810f5261e 100644 --- a/client/app/queue/UnassignedCasesPage.jsx +++ b/client/app/queue/UnassignedCasesPage.jsx @@ -29,6 +29,8 @@ import Alert from '../components/Alert'; import LoadingContainer from '../components/LoadingContainer'; import { LOGO_COLORS } from '../constants/AppConstants'; import { css } from 'glamor'; +import querystring from 'querystring'; +import { columnsFromConfig } from './queueTableUtils'; import { DEFAULT_QUEUE_TABLE_SORT } from './constants'; const assignSectionStyling = css({ marginTop: '30px' }); @@ -77,6 +79,49 @@ class UnassignedCasesPage extends React.PureComponent { const HeadingTag = userIsSCTCoordinator ? 'h1' : 'h2'; + // Setup for backend paginated task retrieval + const tabPaginationOptions = querystring.parse(window.location.search.slice(1)); + const queueConfig = this.props.queueConfig; + const tabConfig = queueConfig.tabs?.[0] || {}; + const tabColumns = columnsFromConfig(queueConfig, tabConfig, []); + + if (tabConfig) { + // Order all of the columns from the backend except badges so any included columns will be in front + tabColumns.slice(1).forEach((obj) => { + obj.order = 1; + }); + + // // Setup default sorting for the SCT assign page. + // If there is no sort by column in the pagination options, then use the tab config default sort + if (!tabPaginationOptions.sort_by) { + tabPaginationOptions.sort_by = tabConfig.defaultSort?.sortColName; + tabPaginationOptions.order = tabConfig.defaultSort?.sortAscending ? 'asc' : 'desc'; + } + + } + + const includedColumnProps = { + includeBadges: true, + includeSelect: true, + includeDetailsLink: true, + includeType: true, + includeDocketNumber: true, + includeIssueCount: true, + includeDaysWaiting: true, + includeIssueTypes: Boolean(userIsCamoEmployee), + includeReaderLink: true, + includeNewDocsIcon: true, + }; + + const specialtyCaseTeamProps = { + includeSelect: true, + customColumns: tabColumns, + taskPagesApiEndpoint: tabConfig.task_page_endpoint_base_path, + useTaskPagesApi: true, + tabPaginationOptions, + useReduxCache: true, + }; + return {JUDGE_QUEUE_UNASSIGNED_CASES_PAGE_TITLE} {error && } @@ -97,17 +142,9 @@ class UnassignedCasesPage extends React.PureComponent { } {!this.props.distributionCompleteCasesLoading && { const { queue: { isTaskAssignedToUserSelected, - pendingDistribution + pendingDistribution, + queueConfig }, ui: { userIsCamoEmployee, @@ -136,6 +174,7 @@ const mapStateToProps = (state, ownProps) => { } = state; let taskSelector = judgeAssignTasksSelector(state); + let fromReduxTasks = true; if (userIsCamoEmployee && isVhaCamoOrg(state)) { taskSelector = camoAssignTasksSelector(state); @@ -143,15 +182,17 @@ const mapStateToProps = (state, ownProps) => { if (userIsSCTCoordinator && isSpecialtyCaseTeamOrg(state)) { taskSelector = specialtyCaseTeamAssignTasksSelector(state); + fromReduxTasks = false; } return { tasks: taskSelector, isTaskAssignedToUserSelected, pendingDistribution, + queueConfig, distributionLoading: pendingDistribution !== null, distributionCompleteCasesLoading: pendingDistribution && pendingDistribution.status === 'completed', - selectedTasks: selectedTasksSelector(state, ownProps.userId), + selectedTasks: selectedTasksSelector(state, ownProps.userId, fromReduxTasks), success, error, userIsCamoEmployee: isVhaCamoOrg(state), @@ -178,6 +219,7 @@ UnassignedCasesPage.propTypes = { title: PropTypes.string, detail: PropTypes.string }), + queueConfig: PropTypes.object, userIsCamoEmployee: PropTypes.bool, userIsSCTCoordinator: PropTypes.bool, isVhaCamoOrg: PropTypes.bool, diff --git a/client/app/queue/caching/cachingReducer.js b/client/app/queue/caching/cachingReducer.js new file mode 100644 index 00000000000..7cb4860ea94 --- /dev/null +++ b/client/app/queue/caching/cachingReducer.js @@ -0,0 +1,8 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import queueTableCacheReducer from './queueTableCache.slice'; + +const cachingReducer = combineReducers({ + queueTable: queueTableCacheReducer, +}); + +export default cachingReducer; diff --git a/client/app/queue/caching/queueTableCache.slice.js b/client/app/queue/caching/queueTableCache.slice.js new file mode 100644 index 00000000000..a61beb8f00b --- /dev/null +++ b/client/app/queue/caching/queueTableCache.slice.js @@ -0,0 +1,119 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + cachedResponses: {}, +}; + +// Helper functions for removing tasks from the cachedResponses. +const generateKeyFromUrl = (urlString) => { + // Split the url to only have get params from the cached url key + const params = new URLSearchParams(urlString.split('?')[1]); + const filter = params.get('filter'); + const tab = params.get('tab'); + + // Create a group key based on filter and tab values + const groupKey = `filter=${filter}&tab=${tab}`; + + return groupKey; +}; + +const groupUrlKeysByParams = (urlKeys) => { + const groups = {}; + + Object.keys(urlKeys).forEach((urlString) => { + const groupKey = generateKeyFromUrl(urlString); + + // Initialize the group if it doesn't exist + if (!groups[groupKey]) { + groups[groupKey] = { + urls: [], + count: 0 + }; + } + + // Add the URL key to the corresponding group + groups[groupKey].urls.push(urlString); + }); + + return groups; +}; + +const addToUrlCount = (groupedUrls, urlString, number) => { + const groupKey = generateKeyFromUrl(urlString); + + // Keep a grouped count of shared tasks between similar cached task pages + if (groupedUrls[groupKey]) { + groupedUrls[groupKey].count += number; + } +}; + +const createUrlCountObject = (groupedUrls) => { + const urlCountObject = {}; + + Object.values(groupedUrls).forEach((group) => { + group.urls.forEach((urlString) => { + urlCountObject[urlString] = group.count; + }); + }); + + return urlCountObject; +}; + +const calculatePages = (itemsPerPage, totalItems) => { + return Math.ceil(totalItems / itemsPerPage); +}; +// End of helper functions + +const resetState = () => ({ ...initialState }); + +const queueTableCacheSlice = createSlice({ + name: 'queueTableCache', + initialState, + reducers: { + reset: resetState, + updateQueueTableCache: (state, action) => { + const { key, value } = action.payload; + + state.cachedResponses[key] = value; + }, + removeTaskIdsFromCache: (state, action) => { + const { taskIds } = action.payload; + + const groupedKeys = groupUrlKeysByParams(state.cachedResponses); + + Object.keys(state.cachedResponses).forEach((key) => { + const cachedResponse = state.cachedResponses[key]; + + if (cachedResponse.tasks && Array.isArray(cachedResponse.tasks)) { + const filteredTasks = cachedResponse.tasks.filter((task) => !taskIds.includes(task.id)); + const numberOfExcludedRecords = cachedResponse.tasks.length - filteredTasks.length; + + state.cachedResponses[key].tasks = filteredTasks; + + addToUrlCount(groupedKeys, key, numberOfExcludedRecords); + } + }); + + const urlsWithRemovedTaskCount = createUrlCountObject(groupedKeys); + + Object.keys(state.cachedResponses).forEach((key) => { + const cachedResponse = state.cachedResponses[key]; + const itemsPerPage = cachedResponse.tasks_per_page; + + if (cachedResponse.tasks && Array.isArray(cachedResponse.tasks)) { + state.cachedResponses[key].total_task_count -= urlsWithRemovedTaskCount[key]; + state.task_page_count = calculatePages(itemsPerPage, state.cachedResponses[key].total_task_count); + } + }); + + } + }, +}); + +export const { + reset, + updateQueueTableCache, + removeTaskIdsFromCache +} = queueTableCacheSlice.actions; + +export default queueTableCacheSlice.reducer; diff --git a/client/app/queue/components/TaskTable.jsx b/client/app/queue/components/TaskTable.jsx index f5d84a4b422..ed44160b886 100644 --- a/client/app/queue/components/TaskTable.jsx +++ b/client/app/queue/components/TaskTable.jsx @@ -25,6 +25,7 @@ import { import { setSelectionOfTaskOfUser } from '../QueueActions'; import { hasDASRecord } from '../utils'; import COPY from '../../../COPY'; +import { updateQueueTableCache } from '../caching/queueTableCache.slice'; export class TaskTableUnconnected extends React.PureComponent { getKeyForRow = (rowNumber, object) => object.uniqueId @@ -34,7 +35,7 @@ export class TaskTableUnconnected extends React.PureComponent { } const isTaskSelected = this.props.isTaskAssignedToUserSelected[this.props.userId] || {}; - return isTaskSelected[uniqueId] || false; + return isTaskSelected[uniqueId]?.selected || false; } taskHasDASRecord = (task) => { @@ -57,7 +58,8 @@ export class TaskTableUnconnected extends React.PureComponent { onChange={(selected) => this.props.setSelectionOfTaskOfUser({ userId: this.props.userId, taskId: task.uniqueId, - selected + selected, + task })} /> } : null; } @@ -119,20 +121,23 @@ export class TaskTableUnconnected extends React.PureComponent { } } - render = () => - (this.taskHasDASRecord(task) || !this.props.requireDasRecord) ? null : 'usa-input-error'} - taskPagesApiEndpoint={this.props.taskPagesApiEndpoint} - useTaskPagesApi={this.props.useTaskPagesApi} - tabPaginationOptions={this.props.tabPaginationOptions} - />; + render = () => + (this.taskHasDASRecord(task) || !this.props.requireDasRecord) ? null : 'usa-input-error'} + taskPagesApiEndpoint={this.props.taskPagesApiEndpoint} + useTaskPagesApi={this.props.useTaskPagesApi} + tabPaginationOptions={this.props.tabPaginationOptions} + useReduxCache={this.props.useReduxCache} + reduxCache={this.props.queueTableResponseCache} + updateReduxCache={this.props.updateQueueTableCache} + />; } TaskTableUnconnected.propTypes = { @@ -165,17 +170,21 @@ TaskTableUnconnected.propTypes = { tabPaginationOptions: PropTypes.object, onHistoryUpdate: PropTypes.func, preserveQueueFilter: PropTypes.bool, + queueTableResponseCache: PropTypes.object, + updateQueueTableCache: PropTypes.func, + useReduxCache: PropTypes.bool, }; const mapStateToProps = (state) => ({ isTaskAssignedToUserSelected: state.queue.isTaskAssignedToUserSelected, userIsVsoEmployee: state.ui.userIsVsoEmployee, userRole: state.ui.userRole, - organizationId: state.ui.activeOrganization.id + organizationId: state.ui.activeOrganization.id, + queueTableResponseCache: state.caching.queueTable.cachedResponses }); const mapDispatchToProps = (dispatch) => ( - bindActionCreators({ setSelectionOfTaskOfUser }, dispatch) + bindActionCreators({ setSelectionOfTaskOfUser, updateQueueTableCache }, dispatch) ); export default (connect(mapStateToProps, mapDispatchToProps)(TaskTableUnconnected)); diff --git a/client/app/queue/queueTableUtils.js b/client/app/queue/queueTableUtils.js new file mode 100644 index 00000000000..2c7748c7257 --- /dev/null +++ b/client/app/queue/queueTableUtils.js @@ -0,0 +1,114 @@ +import { + assignedToColumn, + assignedByColumn, + badgesColumn, + boardIntakeColumn, + completedToNameColumn, + daysOnHoldColumn, + daysSinceLastActionColumn, + daysSinceIntakeColumn, + receiptDateColumn, + daysWaitingColumn, + detailsColumn, + docketNumberColumn, + documentIdColumn, + lastActionColumn, + issueCountColumn, + issueTypesColumn, + readerLinkColumn, + readerLinkColumnWithNewDocsIcon, + regionalOfficeColumn, + taskColumn, + taskOwnerColumn, + taskCompletedDateColumn, + typeColumn, + vamcOwnerColumn +} from './components/TaskTableColumns'; +import QUEUE_CONFIG from '../../constants/QUEUE_CONFIG'; + +const filterValuesForColumn = (column) => + column && column.filterable && column.filter_options; + +export const createColumnObject = (column, config, tasks, requireDasRecord) => { + + const filterOptions = filterValuesForColumn(column); + const functionForColumn = { + [QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name]: typeColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.BADGES.name]: badgesColumn(tasks), + [QUEUE_CONFIG.COLUMNS.CASE_DETAILS_LINK.name]: detailsColumn( + tasks, + requireDasRecord, + config.userRole + ), + [QUEUE_CONFIG.COLUMNS.DAYS_ON_HOLD.name]: daysOnHoldColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_LAST_ACTION.name]: daysSinceLastActionColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name]: daysWaitingColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DOCKET_NUMBER.name]: docketNumberColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DOCUMENT_COUNT_READER_LINK.name]: readerLinkColumn( + requireDasRecord, + true + ), + [QUEUE_CONFIG.COLUMNS.DOCUMENT_ID.name]: documentIdColumn(), + [QUEUE_CONFIG.COLUMNS.ISSUE_COUNT.name]: issueCountColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name]: issueTypesColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.READER_LINK_WITH_NEW_DOCS_ICON. + name]: readerLinkColumnWithNewDocsIcon(requireDasRecord), + [QUEUE_CONFIG.COLUMNS.REGIONAL_OFFICE.name]: regionalOfficeColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name]: assignedToColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.BOARD_INTAKE.name]: boardIntakeColumn( + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.LAST_ACTION.name]: lastActionColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_OWNER.name]: taskOwnerColumn( + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.VAMC_OWNER.name]: vamcOwnerColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNER.name]: completedToNameColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name]: assignedByColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name]: taskCompletedDateColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_TYPE.name]: taskColumn(tasks, filterOptions), + [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_INTAKE.name]: daysSinceIntakeColumn(requireDasRecord), + [QUEUE_CONFIG.COLUMNS.RECEIPT_DATE_INTAKE.name]: receiptDateColumn(), + }; + + return functionForColumn[column.name]; +}; + +export const columnsFromConfig = (config, tabConfig, tasks) => + (tabConfig.columns || []).map((column) => + createColumnObject(column, config, tasks) + ); diff --git a/client/app/queue/reducers.js b/client/app/queue/reducers.js index 1fa4850fa4c..eb705894b9e 100644 --- a/client/app/queue/reducers.js +++ b/client/app/queue/reducers.js @@ -23,6 +23,7 @@ import caseSelectReducer from '../reader/CaseSelect/CaseSelectReducer'; import editClaimantReducer from './editAppellantInformation/editAppellantInformationSlice'; import cavcDashboardReducer from './cavcDashboard/cavcDashboardReducer'; +import cachingReducer from './caching/cachingReducer'; export const initialState = { judges: {}, @@ -550,7 +551,11 @@ const errorTasksAndAppealsOfAttorney = (state, action) => { const setSelectionOfTaskOfUser = (state, action) => { const isTaskSelected = update(state.isTaskAssignedToUserSelected[action.payload.userId] || {}, { [action.payload.taskId]: { - $set: action.payload.selected + $set: { + selected: action.payload.selected, + task: action.payload.task + }, + } }); @@ -807,7 +812,8 @@ const rootReducer = combineReducers({ substituteAppellant: substituteAppellantReducer, cavcRemand: editCavRemandReducer, editClaimantReducer, - cavcDashboard: cavcDashboardReducer + cavcDashboard: cavcDashboardReducer, + caching: cachingReducer }); export default timeFunction( diff --git a/client/app/queue/selectors.js b/client/app/queue/selectors.js index e0f61f64c6c..bb6640a9290 100644 --- a/client/app/queue/selectors.js +++ b/client/app/queue/selectors.js @@ -10,13 +10,20 @@ import COPY from '../../COPY'; const moment = extendMoment(Moment); -export const selectedTasksSelector = (state, userId) => { - return map(state.queue.isTaskAssignedToUserSelected[userId] || {}, (selected, id) => { - if (!selected) { +export const selectedTasksSelector = (state, userId, fromReduxTasks = true) => { + return map(state.queue.isTaskAssignedToUserSelected[userId] || {}, (task, id) => { + if (!task.selected) { return; } - return state.queue.tasks[id] || state.queue.amaTasks[id]; + // If you are pulling the tasks from the serialized redux tasks + if (fromReduxTasks) { + return state.queue.tasks[id] || state.queue.amaTasks[id]; + } + + // Grabbing tasks from the selectedTasksReduxStore instead of the queue tasks store + return task.task; + }).filter(Boolean); }; diff --git a/spec/factories/task.rb b/spec/factories/task.rb index a184edca648..3478613c729 100644 --- a/spec/factories/task.rb +++ b/spec/factories/task.rb @@ -602,6 +602,20 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) associated_attorney { nil } end + trait :advanced_on_docket do + appeal do + create(:appeal, + :with_vha_issue, + :with_post_intake_tasks, + :direct_review_docket, + :advanced_on_docket_due_to_age) + end + end + + trait :cavc_type do + appeal { create(:appeal, :type_cavc_remand) } + end + trait :action_required do after(:create) do |task, evaluator| task.update(status: Constants.TASK_STATUSES.in_progress)