diff --git a/app/hooks/features/index.ts b/app/hooks/features/index.ts index 8f54ff9480..851adc0286 100644 --- a/app/hooks/features/index.ts +++ b/app/hooks/features/index.ts @@ -68,6 +68,7 @@ export function useAllFeatures(projectId, options: UseFeaturesOptionsProps = {}) }); const query = useInfiniteQuery(['all-features', projectId, JSON.stringify(options)], fetchFeatures, { + retry: false, placeholderData: placeholderDataRef.current, getNextPageParam: (lastPage) => { const { data: { meta } } = lastPage; @@ -78,10 +79,10 @@ export function useAllFeatures(projectId, options: UseFeaturesOptionsProps = {}) }, }); - const { data } = query; + const { data, error } = query; const { pages } = data || {}; - if (data) { + if (data || error) { placeholderDataRef.current = data; } diff --git a/app/hooks/gap-analysis/index.ts b/app/hooks/gap-analysis/index.ts index 1b4482c34e..f178c7a770 100644 --- a/app/hooks/gap-analysis/index.ts +++ b/app/hooks/gap-analysis/index.ts @@ -56,6 +56,7 @@ export function useGapAnalysis(projectId, options: UseFeaturesOptionsProps = {}) }); const query = useInfiniteQuery(['gap-analysis', projectId, JSON.stringify(options)], fetchFeatures, { + retry: false, placeholderData: placeholderDataRef.current, getNextPageParam: (lastPage) => { const { data: { meta } } = lastPage; @@ -66,10 +67,10 @@ export function useGapAnalysis(projectId, options: UseFeaturesOptionsProps = {}) }, }); - const { data } = query; + const { data, error } = query; const { pages } = data || {}; - if (data) { + if (data || error) { placeholderDataRef.current = data; } diff --git a/app/hooks/organizations/index.ts b/app/hooks/organizations/index.ts index b54a0aa200..2e1d36f807 100644 --- a/app/hooks/organizations/index.ts +++ b/app/hooks/organizations/index.ts @@ -56,6 +56,7 @@ export function useOrganizations(options: UseOrganizationsOptionsProps = {}) { }); const query = useInfiniteQuery(['organizations', JSON.stringify(options)], fetchOrganizations, { + retry: false, placeholderData: placeholderDataRef.current, getNextPageParam: (lastPage) => { const { data: { meta } } = lastPage; @@ -66,10 +67,10 @@ export function useOrganizations(options: UseOrganizationsOptionsProps = {}) { }, }); - const { data } = query; + const { data, error } = query; const { pages } = data || {}; - if (data) { + if (data || error) { placeholderDataRef.current = data; } diff --git a/app/hooks/scenarios/index.ts b/app/hooks/scenarios/index.ts index 9ff59a6039..e6feea3281 100644 --- a/app/hooks/scenarios/index.ts +++ b/app/hooks/scenarios/index.ts @@ -40,7 +40,7 @@ export function useScenarios(pId, options: UseScenariosOptionsProps = {}) { .reduce((acc, k) => { return { ...acc, - [`filter[${k}]`]: filters[k], + [`filter[${k}]`]: filters[k].toString(), }; }, {}); @@ -63,6 +63,7 @@ export function useScenarios(pId, options: UseScenariosOptionsProps = {}) { }); const query = useInfiniteQuery(['scenarios', pId, JSON.stringify(options)], fetchScenarios, { + retry: false, placeholderData: placeholderDataRef.current, getNextPageParam: (lastPage) => { const { data: { meta } } = lastPage; @@ -73,10 +74,10 @@ export function useScenarios(pId, options: UseScenariosOptionsProps = {}) { }, }); - const { data } = query; + const { data, error } = query; const { pages } = data || {}; - if (data) { + if (data || error) { placeholderDataRef.current = data; } diff --git a/app/layout/projects/show/scenarios/component.tsx b/app/layout/projects/show/scenarios/component.tsx index fe031bd804..862f60387a 100644 --- a/app/layout/projects/show/scenarios/component.tsx +++ b/app/layout/projects/show/scenarios/component.tsx @@ -33,7 +33,7 @@ export const ProjectScenarios: React.FC = () => { const [modal, setModal] = useState(false); const [deleteScenario, setDelete] = useState(null); - const { search } = useSelector((state) => state['/projects/[id]']); + const { search, filters, sort } = useSelector((state) => state['/projects/[id]']); const { query } = useRouter(); const { pid } = query; @@ -63,8 +63,9 @@ export const ProjectScenarios: React.FC = () => { search, filters: { projectId: pid, + ...filters, }, - sort: '-lastModifiedAt', + sort, }); const scrollRef = useBottomScrollListener( @@ -150,7 +151,7 @@ export const ProjectScenarios: React.FC = () => { @@ -167,7 +168,7 @@ export const ProjectScenarios: React.FC = () => {
- {allScenariosData.map((s, i) => { + {!!allScenariosData.length && allScenariosData.map((s, i) => { return ( = () => { /> ); })} + + {!allScenariosData.length && ( +
+ No results found +
+ )}
; + onChangeFilters: (filters: Record) => void; + sort?: string; + onChangeSort: (sort: string) => void; + onDismiss?: () => void; +} + +const STATUS = [ + { id: 'created', label: 'created' }, + { id: 'running', label: 'running' }, + { id: 'completed', label: 'completed' }, +]; + +const SORT = [ + { id: '-lastModifiedAt', label: 'Most recent' }, + { id: 'lastModifiedAt', label: 'First created' }, + { id: 'name', label: 'Name' }, + { id: '-name', label: '-Name' }, +]; + +export const ProjectScenariosFilters: React.FC = ({ + filters = {}, + onChangeFilters, + sort, + onChangeSort, + onDismiss, +}: ProjectScenariosFiltersProps) => { + const INITIAL_VALUES = useMemo(() => { + return { + ...filters, + sort: sort || SORT[0].id, + }; + }, [filters, sort]); + + // Callbacks + const onSubmit = useCallback((values) => { + const { sort: valuesSort, ...valuesFilters } = values; + onChangeFilters(valuesFilters); + onChangeSort(valuesSort); + if (onDismiss) onDismiss(); + }, [onChangeFilters, onChangeSort, onDismiss]); + + const onClear = useCallback(() => { + onChangeFilters({}); + onChangeSort(null); + if (onDismiss) onDismiss(); + }, [onChangeFilters, onChangeSort, onDismiss]); + + return ( + + {({ handleSubmit }) => ( +
+

Filters

+ +
+
+

Filter by type

+
+ {STATUS.map(({ id, label }) => { + return ( + + {(fprops) => ( +
+ + +
+ )} +
+ ); + })} +
+
+ +
+

Order by

+
+ {SORT.map(({ id, label }) => { + return ( + + {(fprops) => ( +
+ + +
+ )} +
+ ); + })} +
+
+
+ +
+ + + +
+
+ )} +
+ ); +}; + +export default ProjectScenariosFilters; diff --git a/app/layout/projects/show/scenarios/filters/index.ts b/app/layout/projects/show/scenarios/filters/index.ts new file mode 100644 index 0000000000..b404d7fd44 --- /dev/null +++ b/app/layout/projects/show/scenarios/filters/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/app/layout/projects/show/scenarios/toolbar/component.tsx b/app/layout/projects/show/scenarios/toolbar/component.tsx index ba3c92c0f8..39c181898d 100644 --- a/app/layout/projects/show/scenarios/toolbar/component.tsx +++ b/app/layout/projects/show/scenarios/toolbar/component.tsx @@ -1,22 +1,58 @@ -import React, { useEffect } from 'react'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { useSelector, useDispatch } from 'react-redux'; -import { setSearch } from 'store/slices/projects/detail'; +import { setSearch, setFilters, setSort } from 'store/slices/projects/[id]'; import Search from 'components/search'; +import Icon from 'components/icon'; +import Modal from 'components/modal'; + +import Filters from 'layout/projects/show/scenarios/filters'; + +import FILTER_SVG from 'svgs/ui/filter.svg?sprite'; export interface ProjectScenariosToolbarProps { } export const ProjectScenariosToolbar: React.FC = () => { - const { search } = useSelector((state) => state['/projects/[id]']); + const { search, filters, sort } = useSelector((state) => state['/projects/[id]']); const dispatch = useDispatch(); - const onChangeDebounced = useDebouncedCallback((value) => { + const [open, setOpen] = useState(false); + const FILTERS_LENGTH = useMemo(() => { + if (!filters) return 0; + + return Object.keys(filters) + .reduce((acc, k) => { + if (typeof filters[k] === 'undefined') return acc; + + if (filters[k] && Array.isArray(filters[k])) { + return acc + filters[k].length; + } + + return acc + 1; + }, 0); + }, [filters]); + + const onChangeOpen = useCallback(() => { + setOpen(true); + }, []); + + const onChangeSearchDebounced = useDebouncedCallback((value) => { dispatch(setSearch(value)); }, 500); + const onFilters = useCallback((value) => { + dispatch(setFilters(value)); + }, [dispatch]); + + const onSort = useCallback((value) => { + dispatch(setSort(value)); + }, [dispatch]); + useEffect(() => { // setSearch to null wheneverer you unmount this component return function unmount() { @@ -25,13 +61,47 @@ export const ProjectScenariosToolbar: React.FC = ( }, [dispatch]); return ( - +
+ + + + + { + setOpen(false); + }} + > + + +
); }; diff --git a/app/layout/scenarios/sidebar/features/add/filters/component.tsx b/app/layout/scenarios/sidebar/features/add/filters/component.tsx index ab3fe5a956..562ed2e0e6 100644 --- a/app/layout/scenarios/sidebar/features/add/filters/component.tsx +++ b/app/layout/scenarios/sidebar/features/add/filters/component.tsx @@ -36,7 +36,7 @@ export const ScenarioFeaturesAddFilters: React.FC { return { ...filters, - sort: sort || 'alias', + sort: sort || SORT[0].id, }; }, [filters, sort]); diff --git a/app/layout/scenarios/sidebar/features/add/toolbar/component.tsx b/app/layout/scenarios/sidebar/features/add/toolbar/component.tsx index d54f766ebe..a0b1d852cc 100644 --- a/app/layout/scenarios/sidebar/features/add/toolbar/component.tsx +++ b/app/layout/scenarios/sidebar/features/add/toolbar/component.tsx @@ -33,6 +33,8 @@ export const ScenarioFeaturesAddToolbar: React.FC { const [open, setOpen] = useState(false); const FILTERS_LENGTH = useMemo(() => { + if (!filters) return 0; + return Object.keys(filters) .reduce((acc, k) => { if (typeof filters[k] === 'undefined') return acc; diff --git a/app/store/index.ts b/app/store/index.ts index 221e7bb5a4..f6855e9a8b 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,7 +1,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import projects from 'store/slices/projects'; -import projectsDetail from 'store/slices/projects/detail'; +import projectsDetail from 'store/slices/projects/[id]'; import projectsNew from 'store/slices/projects/new'; // import scenariosEdit from 'store/slices/scenarios/edit'; diff --git a/app/store/slices/projects/[id].ts b/app/store/slices/projects/[id].ts new file mode 100644 index 0000000000..ba9028ced8 --- /dev/null +++ b/app/store/slices/projects/[id].ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface ProjectShowStateProps { + search: string, + filters: Record; + sort: string; +} + +const initialState = { + search: '', + filters: {}, + sort: '-lastModifiedAt', +} as ProjectShowStateProps; + +const projectsDetailSlice = createSlice({ + name: '/projects/[id]', + initialState, + reducers: { + setSearch: (state, action: PayloadAction) => { + state.search = action.payload; + }, + setFilters: (state, action: PayloadAction>) => { + state.filters = action.payload; + }, + setSort: (state, action: PayloadAction) => { + state.sort = action.payload; + }, + }, +}); + +export const { setSearch, setFilters, setSort } = projectsDetailSlice.actions; +export default projectsDetailSlice.reducer; diff --git a/app/store/slices/projects/detail.ts b/app/store/slices/projects/detail.ts deleted file mode 100644 index e6fd6d9639..0000000000 --- a/app/store/slices/projects/detail.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -interface ProjectShowStateProps { - search: string -} - -const initialState = { search: '' } as ProjectShowStateProps; - -const projectsDetailSlice = createSlice({ - name: '/projects/[id]', - initialState, - reducers: { - setSearch: (state, action: PayloadAction) => { - state.search = action.payload; - }, - }, -}); - -export const { setSearch } = projectsDetailSlice.actions; -export default projectsDetailSlice.reducer;