Skip to content

Instantly share code, notes, and snippets.

@dereknelson
Created February 10, 2020 19:49
Show Gist options
  • Save dereknelson/222dbd9150d120cf19866901da45ab09 to your computer and use it in GitHub Desktop.
Save dereknelson/222dbd9150d120cf19866901da45ab09 to your computer and use it in GitHub Desktop.
redux normalization
import * as constants from '../Constants'
export const fetchProjectTaskGroups = payload => ({
type: constants.FETCH_PROJECT_TASK_GROUPS.TRIGGER,
payload,
})
export const addTaskGroupToFetchQueue = payload => ({
type: constants.ADD_TASK_GROUP_TO_FETCH_QUEUE,
payload,
})
export const removeTaskGroupFromFetchQueue = payload => ({
type: constants.REMOVE_TASK_GROUP_FROM_FETCH_QUEUE,
payload,
})
export const createProjectTaskGroup = payload => ({
type: constants.CREATE_PROJECT_TASK_GROUP.TRIGGER,
payload,
})
export const updateProjectTaskGroup = payload => ({
type: constants.UPDATE_PROJECT_TASK_GROUP.TRIGGER,
payload,
})
export const deleteProjectTaskGroup = payload => ({
type: constants.DELETE_PROJECT_TASK_GROUP.TRIGGER,
payload,
})
export const toggleTaskGroupCollapse = payload => ({
type: constants.TOGGLE_TASK_GROUP,
payload,
})
export const toggleTaskGroupCollapseAll = payload => ({
type: constants.TOGGLE_TASK_GROUP_ALL,
payload,
})
export const fetchProjectTaskGroupTasks = payload => ({
type: constants.FETCH_PROJECT_TASK_GROUP_TASKS.TRIGGER,
payload,
})
export const fetchAllProjectTaskGroups = () => ({
type: constants.FETCH_ALL_PROJECT_TASK_GROUPS.TRIGGER,
})
export const clearTaskGroupFetchQueue = () => ({
type: constants.CLEAR_TASK_GROUP_QUEUE,
})
export const moveTaskToGroup = ({ sourceTaskGroupId, destinationTaskGroupId, taskId, index }) => ({
type: constants.MOVE_TASK_TO_GROUP,
payload: {
sourceTaskGroupId,
destinationTaskGroupId,
taskId,
index,
},
})
export const removeTaskFromGroup = ({ taskGroupId, taskId }) => ({
type: constants.REMOVE_TASK_FROM_GROUP,
payload: {
taskGroupId,
taskId,
},
})
export const addTaskToGroup = ({ taskGroupId, taskId, index }) => ({
type: constants.ADD_TASK_TO_GROUP,
payload: {
taskGroupId,
taskId,
index,
},
})
import * as constants from '../Constants'
import keyBy from 'lodash/keyBy'
import produce from 'immer'
import { filterStates } from '../../Constants'
const byId = item => item.id
const initialState = {
taskGroupOrders: {},
taskGroups: {},
newGroupId: null,
queuedFetches: {},
}
// these functions only work without return values if we're using immer because it modifies the original state object
const filterOutTaskIds = (taskIds, taskGroups) => {
const keyedTaskIds = keyBy(taskIds)
Object.values(taskGroups).forEach(taskGroup => {
taskGroup.task_order = taskGroup.task_order.filter(
taskId => !keyedTaskIds[taskId] && taskId !== 'completed' && taskId !== 'incomplete',
)
})
}
const markTasksAs = (taskIds, taskGroups, updateType, shouldRemove) => {
Object.values(taskGroups).forEach(taskGroup => {
taskIds.map(taskId => {
const index = taskGroup.task_order.findIndex(id => id === taskId)
if (index !== -1) {
if (updateType === 'complete') {
if (shouldRemove) {
taskGroup.task_order[index] = 'completed'
} else {
taskGroup.task_order.splice(index, 1)
taskGroup.current_page >= taskGroup.total_pages && taskGroup.task_order.push(taskId)
}
}
if (updateType === 'incomplete') {
if (shouldRemove) {
taskGroup.task_order[index] = 'incomplete'
} else {
taskGroup.task_order.splice(index, 1)
taskGroup.task_order.unshift(taskId)
}
}
}
})
})
}
const moveTaskToTop = (taskId, taskGroups) => {
Object.values(taskGroups).forEach(taskGroup => {
if (taskGroup.task_order.includes(taskId)) {
taskGroup.task_order = taskGroup.task_order.filter(id => id !== taskId)
taskGroup.task_order.unshift(taskId)
}
})
}
export default (state = initialState, action) => {
return produce(state, draft => {
switch (action.type) {
case constants.FETCH_PROJECT_TASK_GROUPS.SUCCESS: {
const {
response: { task_groups = [], task_group_order },
initialActionPayload: { preventOverwrite, projectId },
} = action.payload
draft.taskGroupOrders[projectId] = task_group_order
const theGroups = task_groups.map(group => {
return {
...group,
current_page: 0,
task_order:
preventOverwrite &&
draft.taskGroups[group.id] &&
draft.taskGroups[group.id].task_order
? draft.taskGroups[group.id].task_order
: [],
collapsed:
preventOverwrite &&
draft.taskGroups[group.id] &&
draft.taskGroups[group.id].collapsed,
total_unfiltered: group.task_order.length,
}
})
draft.taskGroups = { ...draft.taskGroups, ...keyBy(theGroups, byId) }
break
}
case constants.CREATE_PROJECT_TASK_GROUP.SUCCESS: {
const {
response,
requestPayload: { token, projectId, name, index = 0 },
} = action.payload
draft.newGroupId = response.id
if (index === 0 && draft.taskGroupOrders[projectId][0] === response.id) {
draft.taskGroups[response.id].is_default = false
break // backend actually updated existing default group, do not insert id
}
draft.taskGroups[response.id] = {
...response,
current_page: 0,
task_order: [],
}
draft.taskGroupOrders[projectId].splice(index, 0, response.id)
break
}
case constants.DELETE_PROJECT_TASK_GROUP.TRIGGER: {
draft.newGroupId = null
const { projectId, id } = action.payload
draft.taskGroupOrders[projectId] = draft.taskGroupOrders[projectId].filter(
groupId => groupId !== id,
)
delete draft.taskGroups[id]
break
}
case constants.TOGGLE_TASK_GROUP: {
const { groupId, collapsed } = action.payload
draft.taskGroups[groupId].collapsed = collapsed
draft.taskGroups[groupId].isFetching = false
break
}
case constants.TOGGLE_TASK_GROUP_ALL: {
const { collapsed } = action.payload
Object.values(draft.taskGroups).forEach(taskGroup => {
taskGroup.collapsed = collapsed
})
break
}
case constants.UPDATE_PROJECT_TASK_GROUP.TRIGGER: {
draft.newGroupId = null
const { id } = action.payload
draft.taskGroups[id] = {
...draft.taskGroups[id],
...action.payload,
}
break
}
case constants.FETCH_PROJECT_TASK_GROUP_TASKS.REQUEST: {
const { taskGroups } = draft
const { request } = action.payload
const [groupId] = request.task_group_ids
const group = taskGroups[groupId]
if (
group &&
group.task_order &&
(group.task_order.length === 0 ||
group.task_order.length <= group.project_tasks_count - 1) &&
!group.collapsed
) {
draft.queuedFetches[groupId] = true
group.isFetching = true
break
}
break
}
case constants.ADD_TASK_GROUP_TO_FETCH_QUEUE:
{
const { groupId } = action.payload
draft.queuedFetches[groupId] = true
}
break
case constants.REMOVE_TASK_GROUP_FROM_FETCH_QUEUE: {
const { groupId } = action.payload
delete draft.queuedFetches[groupId]
break
}
case constants.FETCH_PROJECT_TASK_GROUP_TASKS.SUCCESS: {
const {
meta: {
response: { tasks, current_page, total_pages },
action: {
payload: { groupIds },
},
},
} = action.payload
/*
* if there are no tasks in the response (because none meet the filter/sort criteria), the logic `tasks.map` of clearing "isFetching" etc. will never be called,
* leading to what appears to be infinite loading state. if there is more than one group in the groupIds array, the current/total page count will be
* for that request & therefore be inaccurate, preventing us from relying on those counts to determine when we need to stop fetching the current
* group and begin fetching the next one
*/
if (groupIds && groupIds[0] && groupIds.length === 1) {
draft.taskGroups[groupIds[0]].isFetching = false
draft.taskGroups[groupIds[0]].total_pages = total_pages
draft.taskGroups[groupIds[0]].current_page = current_page
}
tasks.map(task => {
const currentGroup = draft.taskGroups[task.task_group_id]
if (currentGroup) {
currentGroup.current_page = current_page
currentGroup.total_pages = total_pages
currentGroup.task_order.push(task.id)
currentGroup.isFetching = false
if (currentGroup.current_page >= currentGroup.total_pages) {
delete draft.queuedFetches[currentGroup.id]
}
}
})
Object.values(draft.taskGroups).forEach(taskGroup => {
taskGroup.task_order = [...new Set(taskGroup.task_order)]
})
break
}
case constants.CREATE_TASK.TRIGGER: {
const { payload } = action
if (payload.task_group_id) {
const group = draft.taskGroups[payload.task_group_id] || {}
group.task_order = [payload.temp_id, ...group.task_order]
}
break
}
case constants.CREATE_TASK.SUCCESS: {
const {
payload: {
response: { task },
},
} = action
if (task.task_group_id) {
const group = draft.taskGroups[task.task_group_id]
if (!group) return
const index = group.task_order.findIndex(id => id === task.temp_id)
group.task_order[index] = task.id
}
break
}
case constants.BATCH_DELETE_TASKS.TRIGGER: {
const { taskIds } = action.payload
filterOutTaskIds(taskIds, draft.taskGroups, draft.taskGroupOrders)
break
}
case constants.TASKS_MOVE_TO_PROJECT.TRIGGER: {
const { payload } = action
const { task_ids = [], task_group_id } = payload || {}
if (!task_group_id) break
const taskIds = keyBy(task_ids)
Object.values(draft.taskGroups).map(group => {
group.task_order = group.task_order.filter(id => id !== taskIds[id])
})
const taskGroup = draft.taskGroups[task_group_id]
if (taskGroup) taskGroup.task_order = task_ids.concat(taskGroup.task_order)
break
}
case constants.REMOVE_TASK_FROM_GROUP: {
const { taskId, taskGroupId } = action.payload
const { taskGroups } = draft
const taskGroup = taskGroups[taskGroupId]
taskGroup.task_order = taskGroup.task_order.filter(id => id != taskId)
break
}
case constants.ADD_TASK_TO_GROUP: {
const { taskId, taskGroupId, index } = action.payload
const { taskGroups } = draft
const taskGroup = taskGroups[taskGroupId]
taskGroup.task_order.splice(index, 0, taskId)
break
}
case constants.HANDLE_TASK_GROUPS_TASK_COMPLETION_TOGGLE: {
const { taskIds, isCompleting, currentFilter } = action.payload
const { state: filterState } = currentFilter
const updateType = isCompleting ? 'complete' : 'incomplete'
const shouldRemove =
(filterState === 'completed' && !isCompleting) ||
(filterState === 'incomplete' && isCompleting)
markTasksAs(taskIds, draft.taskGroups, updateType, shouldRemove)
break
}
case constants.CLEAR_REMOVED_TASKS: {
filterOutTaskIds([], draft.taskGroups, draft.taskGroupOrders)
break
}
case constants.CLEAR_TASK_GROUP_QUEUE: {
draft.queuedFetches = {}
break
}
case constants.UPDATE_TASKS_ATTRIBUTES.TRIGGER: {
const {
body: { task_group_id, task_ids, project_id },
} = action.payload
if (!task_group_id) break
const taskGroups = draft.taskGroupOrders[project_id]
if (taskGroups) {
const taskIds = keyBy(task_ids)
taskGroups.map(groupId => {
const taskGroup = draft.taskGroups[groupId]
taskGroup.task_order = taskGroup.task_order.filter(id => !taskIds[id])
})
if (draft.taskGroups[task_group_id]) {
draft.taskGroups[task_group_id].task_order = task_ids.concat(
draft.taskGroups[task_group_id].task_order,
)
}
}
break
}
case constants.TASK_MOVE_TO_TOP.TRIGGER: {
const { taskIds } = action.payload
const ids = keyBy(taskIds)
Object.values(draft.taskGroups).map(group => {
const idsToMove = []
group.task_order.map(id => {
if (ids[id]) idsToMove.push(id)
})
group.task_order = [...idsToMove, ...group.task_order.filter(id => !ids[id])]
})
break
}
case constants.FLUSH_TASK_LIST_STORE: {
Object.values(draft.taskGroups).forEach(taskGroup => {
taskGroup.task_order = []
taskGroup.current_page = 0
taskGroup.total_pages = undefined
taskGroup.isFetching = false
})
break
}
case constants.REMOVE_UNDO_TASK: {
const { task = { updatedTask: {} }, filter = { state: filterStates.INCOMPLETE } } = action
const { id, completed_at, task_group_id } = task.updatedTask
const group = draft.taskGroups[task_group_id]
if (completed_at === null && filter.state === filterStates.COMPLETED)
filterOutTaskIds([id], draft.taskGroups)
if (completed_at && filter.state === filterStates.INCOMPLETE && group) {
group.task_order = [...group.task_order.filter(taskId => taskId !== id), id]
}
}
}
})
}
import * as api from '../Services/Api'
import * as apiv2 from '../Services/Api.v2'
import produce from 'immer'
import * as entityActions from '../Redux/Actions'
const {
fetchProjectTaskGroups: fetchProjectTaskGroupsEntityAction,
createProjectTaskGroup: createProjectTaskGroupEntityAction,
updateProjectTaskGroup: updateProjectTaskGroupEntityAction,
deleteProjectTaskGroup: deleteProjectTaskGroupEntityAction,
fetchAllProjectTaskGroups: fetchAllProjectTaskGroupsEntityAction,
taskGroupPreferencesFetch: taskGroupPreferencesFetchEntityAction,
taskGroupPreferenceCreate: taskGroupPreferenceCreateEntityAction,
taskGroupPreferenceUpdate: taskGroupPreferenceUpdateEntityAction,
taskGroupPreferenceDelete: taskGroupPreferenceDeleteEntityAction,
updateTasksAttributesEntityActions,
} = entityActions
import {
updateTaskGroupPreference as updateTaskGroupPreferenceActionCreator,
createTaskGroupPreference as createTaskGroupPreferenceActionCreator,
} from '../Redux/Actions/userThemeActions'
import { fetchEntity, changeEntity, apiRequestSaga } from './helpers'
import { select, put, call } from 'redux-saga/effects'
import {
fetchProjectTaskGroups as fetchProjectTaskGroupsActionCreator,
addTaskGroupToFetchQueue,
removeTaskGroupFromFetchQueue,
fetchProjectTaskGroupTasks as fetchProjectTaskGroupTasksActionCreator,
} from '../Redux/Actions/projectTaskGroupActions'
import * as actionCreators from '../Redux/Actions/projectTaskGroupActions'
import { getProjectTaskGroupsState, getAuthToken, getTaskFetchConfig } from '../Redux/Selectors'
export function* fetchProjectTaskGroups(action) {
const { projectId } = action.payload
const { error } = yield fetchEntity(
fetchProjectTaskGroupsEntityAction,
api.fetchProjectTaskGroups,
[projectId],
action,
)
if (!error) {
const { taskGroupOrders, taskGroups } = yield select(getProjectTaskGroupsState)
const taskGroupOrder = taskGroupOrders[projectId]
/*
* this finds the first task group that has tasks because if the viewableItems in the list don't change after the componentDidMount fetch
* we won't start fetching the next group because there is nothing to trigger the call
*/
let groupId = taskGroupOrders[projectId][0]
taskGroupOrder.find(id => {
if (taskGroups[id].total_unfiltered != 0) {
groupId = id
return true
}
})
yield put(fetchProjectTaskGroupTasksActionCreator({ groupId, projectId }))
}
}
export function* fetchProjectTaskGroupTasksQueue(action) {
const { projectId, groupId } = action.payload
const { taskGroupOrders, queuedFetches } = yield select(getProjectTaskGroupsState)
const taskGroupOrder = taskGroupOrders[projectId]
const nextActiveFetch = taskGroupOrder.find(id => queuedFetches[id])
if (!nextActiveFetch || nextActiveFetch === groupId) {
yield call(fetchProjectTaskGroupTasks, action)
} else {
yield put(addTaskGroupToFetchQueue({ groupId }))
}
}
function* callTaskGroupFetchQueueNext(action) {
const { taskGroupOrders, queuedFetches } = yield select(getProjectTaskGroupsState)
const { groupId, projectId } = action.payload
const nextGroupInQueueId = taskGroupOrders[projectId].find(
id => queuedFetches[id] && id !== groupId,
)
if (nextGroupInQueueId) {
yield put(
fetchProjectTaskGroupTasksActionCreator({
projectId,
groupId: nextGroupInQueueId,
}),
)
}
}
export function* fetchProjectTaskGroupTasksSuccess(action) {
const { meta } = action.payload
const { current_page, total_pages } = meta.response
const initialPayload = meta.action.payload
const { groupId, projectId } = initialPayload
if (current_page >= total_pages) {
/* always try to grab next fetch from queue if current group is done
if none exists, no fetch will occur and action terminates gracefully.
waypoints already on page will exist in queue and fetch as necessary to fill page
waypoints not on page will trigger fetch on entering page
*/
yield put(removeTaskGroupFromFetchQueue({ groupId }))
yield call(callTaskGroupFetchQueueNext, {
payload: { groupId, projectId },
})
}
}
function* fetchProjectTaskGroupTasks(action) {
const { taskGroups } = yield select(getProjectTaskGroupsState)
const { sort, filter, selectedAccountIds, ...restConfigProps } = yield select(getTaskFetchConfig)
const { projectId } = action.payload
const page =
taskGroups[action.payload.groupId] && taskGroups[action.payload.groupId].current_page + 1
const newAction = produce(action, theAction => {
theAction.payload.groupIds = [action.payload.groupId]
})
const body = {
project_id: projectId,
task_group_ids: [action.payload.groupId],
...restConfigProps,
state: 'incomplete',
limit: 25,
page, // important that page comes after restConfigProps because taskFetchConfig has a page key that is different than the page logic here
...filter,
// sort
}
const options = {
method: 'POST',
body,
}
const endpoint = `tasks`
yield call(apiRequestSaga, apiv2.fetchTasks, body, newAction)
}
export function* createProjectTaskGroup(action) {
const { projectId, name, index, color } = action.payload
const token = yield select(getAuthToken)
const { error, response } = yield changeEntity(
createProjectTaskGroupEntityAction,
api.createProjectTaskGroup,
[token, projectId, name, index],
action,
)
if (!error && color) {
const newAction = produce(action.payload, draft => {
draft.id = response.id
})
yield put(createTaskGroupPreferenceActionCreator(newAction))
}
}
export function* updateProjectTaskGroup(action) {
const { projectId, name, id } = action.payload
const token = yield select(getAuthToken)
const { error } = yield changeEntity(
updateProjectTaskGroupEntityAction,
api.updateProjectTaskGroup,
[token, id, name, projectId],
action,
)
}
export function* deleteProjectTaskGroup(action) {
const { id, projectId } = action.payload
const token = yield select(getAuthToken)
const { error } = yield changeEntity(
deleteProjectTaskGroupEntityAction,
api.deleteProjectTaskGroup,
[token, id],
action,
)
const { taskGroupOrders } = yield select(getProjectTaskGroupsState)
if (taskGroupOrders[projectId].length === 0) {
yield put(fetchProjectTaskGroupsActionCreator({ projectId }))
}
}
export function* moveTaskToGroupWorker(action) {
const { sourceTaskGroupId, destinationTaskGroupId, taskId, index } = action.payload
yield put(
actionCreators.removeTaskFromGroup({
taskGroupId: sourceTaskGroupId,
taskId,
}),
)
yield put(
actionCreators.addTaskToGroup({
taskGroupId: destinationTaskGroupId,
taskId,
index,
}),
)
}
export function* addTaskToGroupWorker(action) {
const { taskGroupId, taskId, index } = action.payload
const token = yield select(getAuthToken)
const body = {
task_ids: [taskId],
position: index,
task_group_id: taskGroupId,
}
const { error } = yield changeEntity(updateTasksAttributesEntityActions, apiv2.editTask, [
{ token, body },
])
}
export function* fetchTaskGroupPreferences(action) {
const token = yield select(getAuthToken)
const { error, response } = yield fetchEntity(
taskGroupPreferencesFetchEntityAction,
api.fetchTaskGroupPreferences,
[token],
action,
)
}
export function* updateTaskGroupPreference(action) {
const { id, color } = action.payload
const token = yield select(getAuthToken)
const { error, response } = yield changeEntity(
taskGroupPreferenceUpdateEntityAction,
api.updateTaskGroupPreference,
[token, id, color],
action,
)
}
export function* createTaskGroupPreference(action) {
const { id, color } = action.payload
const token = yield select(getAuthToken)
const { error, response } = yield changeEntity(
taskGroupPreferenceCreateEntityAction,
api.createTaskGroupPreference,
[token, id, color],
action,
)
}
export function* deleteTaskGroupPreference(action) {
const { id, color } = action.payload
const token = yield select(getAuthToken)
const { error, response } = yield changeEntity(
taskGroupPreferenceDeleteEntityAction,
api.deleteTaskGroupPreference,
[token, id, color],
action,
)
}
export const getTasks = state => state.tasksList.tasks
export const getEditingTaskId = state => state.tasksList.editingTaskId
export const getPropsTaskId = (state, ownProps) =>
ownProps && (ownProps.taskId || (ownProps.task && ownProps.taskId))
export const getNewTaskComments = (state, ownProps) => ownProps.newTaskComments || emptyArray
export const getFilterState = state => state.tasksList.filter
export const getSearchText = state => state.tasksList.searchText
export const getProjectTasksMap = state => state.tasksList.projectTasks || emptyObj
export const getSelectedGroup = state => state.groups.selectedGroupId
export const getTaskListBatchState = state => state.tasksList.batchMode
export const getTaskGroupFromProps = (state, ownProps) =>
(ownProps && ownProps.group) || { task_order: emptyArray }
export const getPrevTaskFromTasksList = state => state.tasksList.prevTask
// PROJECT GETTERS
export const getSelectedProject = state => state.projects.selectedProject
export const getProjectTaskGroupsState = state => state.projectTaskGroups
export const makeGetTaskFromTasksList = () => {
return createSelector(
getTasks,
getProjectTasksMap,
getPropsTaskId,
(tasks, projectTasksMap, taskId) =>
tasks.find(({ id }) => id === taskId) || projectTasksMap[taskId] || {},
)
}
export const makeGetProjectFromTask = () => {
const getTaskFromTasksList = makeGetTaskFromTasksList()
return createSelector(
getTaskFromTasksList,
getMyProjects,
(task, myProjects) => myProjects.find(({ id }) => id === task.project_id),
)
}
export const getTaskGroups = createSelector(
getSelectedProjectTaskGroups,
getSelectedProjectTaskGroupOrder,
getRouterProjectId,
getOwnProjectId,
(taskGroups, taskGroupOrders, projectId, ownProjectId) => {
const theProjectId = projectId || ownProjectId
return taskGroupOrders[theProjectId]
? taskGroupOrders[theProjectId].map(id => taskGroups[id]).filter(group => group)
: emptyArray
},
)
export const getTaskGroupsMap = createSelector(
getSelectedProjectTaskGroups,
getSelectedProjectTaskGroupOrder,
getRouterProjectId,
getOwnProjectId,
(taskGroups, taskGroupOrders, projectId, ownProjectId) => {
const theProjectId = projectId || ownProjectId
const taskMap =
taskGroupOrders[theProjectId] &&
taskGroupOrders[theProjectId].map(id => taskGroups[id]).filter(group => group)
return keyBy(taskMap, byId)
},
)
export const getTaskGroupTasks = createSelector(
getTaskGroups,
getProjectTasksMap,
(taskGroups, projectTasksMap) => {
let tasks = []
taskGroups.map(group => {
tasks.push({ ...group, isHeader: true })
if (!group.collapsed)
group.task_order.map((id, index) =>
tasks.push({
...projectTasksMap[id],
taskGroupIsFetching: group.isFetching,
isLastInGroup: index === getLength(group.task_order) - 1,
}),
)
})
tasks = tasks.filter(task => task)
const stickyHeaderIndices = []
tasks.map((task, index) => {
if (task.isHeader) stickyHeaderIndices.push(index)
})
return { tasks, stickyHeaderIndices }
},
)
export const getIsFetchingTaskGroups = createSelector(
getProjectTaskGroups,
taskGroups => Object.values(taskGroups).some(taskGroup => taskGroup.isFetching),
)
export const getQueuedTaskGroupFetches = createSelector(
getProjectTaskGroupsState,
projectTaskGroups => projectTaskGroups.queuedFetches,
)
export const getAllTaskGroupsCollapsed = createSelector(
getTaskGroupsMap,
taskGroups => Object.values(taskGroups).every(taskGroup => taskGroup.collapsed),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment