diff --git a/app/src/UI/Map2/LayerPicker/LpRecordSet/LpRecordSet.tsx b/app/src/UI/Map2/LayerPicker/LpRecordSet/LpRecordSet.tsx index 91c2f7121..37d629455 100644 --- a/app/src/UI/Map2/LayerPicker/LpRecordSet/LpRecordSet.tsx +++ b/app/src/UI/Map2/LayerPicker/LpRecordSet/LpRecordSet.tsx @@ -4,6 +4,8 @@ import './LpRecordSet.css'; import LpRecordSetOption from './LpRecordSetOption'; import UserSettings from 'state/actions/userSettings/UserSettings'; import { UserRecordSet } from 'interfaces/UserRecordSet'; +import filterRecordsetsByNetworkState from 'utils/filterRecordsetsByNetworkState'; +import { MOBILE } from 'state/build-time-config'; type PropTypes = { closePicker: () => void; @@ -17,12 +19,14 @@ const LpRecordSet = ({ closePicker }: PropTypes) => { const handleToggleVisibility = (id: string) => dispatch(UserSettings.RecordSet.toggleVisibility(id)); const handleCycleColour = (id: string) => dispatch(UserSettings.RecordSet.cycleColourById(id)); const handleToggleLabels = (id: string) => dispatch(UserSettings.RecordSet.toggleLabelVisibility(id)); + const connected = useSelector((state) => state.Network.connected); const recordSets = useSelector((state) => state.UserSettings?.recordSets); const defaultRecordSets: UserRecordSet[] = []; const customRecordSets: UserRecordSet[] = []; const dispatch = useDispatch(); - Object.keys(recordSets).forEach((recordSet) => { + const userIsMobileAndOffline = MOBILE && !connected; + filterRecordsetsByNetworkState(recordSets, userIsMobileAndOffline).forEach((recordSet) => { if (DEFAULT_RECORD_TYPES.includes(recordSet)) { defaultRecordSets.push({ ...recordSets[recordSet], id: recordSet }); } else { diff --git a/app/src/UI/Overlay/Records/Record.tsx b/app/src/UI/Overlay/Records/Record.tsx index 916203d27..233fb5f9f 100644 --- a/app/src/UI/Overlay/Records/Record.tsx +++ b/app/src/UI/Overlay/Records/Record.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react'; import './Record.css'; import { Route, useHistory } from 'react-router'; -import { useSelector } from 'react-redux'; +import { useSelector } from 'utils/use_selector'; import { ActivityForm } from './Activity/Form'; import { ActivityPhotos } from './Activity/Photos'; import { OverlayHeader } from '../OverlayHeader'; @@ -19,12 +19,12 @@ export const Activity = (props) => { const history = useHistory(); const id = history.location.pathname.split(':')[1]?.split('/')[0]; - const failCode = useSelector((state: any) => state.ActivityPage?.failCode); - const activity_ID = useSelector((state: any) => state.ActivityPage?.activity?.activity_id); + const failCode = useSelector((state) => state.ActivityPage?.failCode); + const activity_ID = useSelector((state) => state.ActivityPage?.activity?.activity_id); - const loading = useSelector((state: any) => state.ActivityPage?.loading); - const apiDocsWithSelectOptions = useSelector((state: any) => state.UserSettings?.apiDocsWithSelectOptions); - const apiDocsWithViewOptions = useSelector((state: any) => state.UserSettings?.apiDocsWithViewOptions); + const loading = useSelector((state) => state.ActivityPage?.loading); + const apiDocsWithSelectOptions = useSelector((state) => state.UserSettings?.apiDocsWithSelectOptions); + const apiDocsWithViewOptions = useSelector((state) => state.UserSettings?.apiDocsWithViewOptions); return (
diff --git a/app/src/UI/Overlay/Records/RecordSet/Filter.tsx b/app/src/UI/Overlay/Records/RecordSet/Filter.tsx index d7a2ba5f5..bb4f8af8f 100644 --- a/app/src/UI/Overlay/Records/RecordSet/Filter.tsx +++ b/app/src/UI/Overlay/Records/RecordSet/Filter.tsx @@ -9,9 +9,10 @@ import debounce from 'lodash.debounce'; type PropTypes = { setID: string; id: string; + userOfflineMobile: boolean; }; -const Filter = ({ setID, id }: PropTypes) => { +const Filter = ({ setID, id, userOfflineMobile }: PropTypes) => { const TIME_TO_AUTO_UPDATE_IN_SECONDS = 0.75; /** @@ -69,13 +70,22 @@ const Filter = ({ setID, id }: PropTypes) => { const getFilterType = (filterTypeInState: string) => { switch (filterTypeInState) { case 'tableFilter': - return ; + return ( + + ); case 'spatialFilterUploaded': return ( updateFilter({ filter: e.target.value })} + value={valueInState} > {clientBoundariesToDisplay?.map((option) => (
- {recordSet?.tableFilters?.length > 0 && !onlyFilterIsForDrafts && viewFilters ? ( + {recordSet?.tableFilters?.length > 0 && !onlyFilterIsForDrafts && viewFilters && ( @@ -140,17 +150,22 @@ export const RecordSet = (props) => { {recordSet?.tableFilters.map((filter: any, i) => { if (filter.field !== 'form_status') - return ; + return ( + + ); })}
- ) : ( - <> )}
- + ); diff --git a/app/src/UI/Overlay/Records/RecordSet/RecordTable.tsx b/app/src/UI/Overlay/Records/RecordSet/RecordTable.tsx index 8253ae85a..551a090cd 100644 --- a/app/src/UI/Overlay/Records/RecordSet/RecordTable.tsx +++ b/app/src/UI/Overlay/Records/RecordSet/RecordTable.tsx @@ -1,4 +1,4 @@ -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import './RecordTable.css'; import { activityColumnsToDisplay, @@ -10,35 +10,34 @@ import { RECORDSET_SET_SORT, USER_CLICKED_RECORD, USER_HOVERED_RECORD, USER_TOUC import { validActivitySortColumns, validIAPPSortColumns } from 'sharedAPI/src/misc/sortColumns'; import { detectTouchDevice } from 'utils/detectTouch'; import VisibilityIcon from '@mui/icons-material/Visibility'; +import { RecordSetType } from 'interfaces/UserRecordSet'; +import { useSelector } from 'utils/use_selector'; +import UserRecord from 'interfaces/UserRecord'; -export const RecordTableHeader = (props) => {}; +type PropTypes = { + setID: string; + userOfflineMobile: boolean; +}; -export const RecordTable = (props) => { - const onUserHoveredRecord = (row) => { +export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => { + const onUserHoveredRecord = (row: UserRecord) => { dispatch({ type: USER_HOVERED_RECORD, payload: { recordType: tableType, - id: tableType === 'Activity' ? row.activity_id : row.site_id, + id: tableType === RecordSetType.Activity ? row.activity_id : row.site_id, row: row } }); }; - const unmappedRows = useSelector((state: any) => state.Map?.recordTables?.[props.setID]?.rows); - - const tableType = useSelector((state: any) => state.UserSettings?.recordSets?.[props.setID]?.recordSetType); + const unmappedRows = useSelector((state) => state.Map?.recordTables?.[setID]?.rows); + const tableType = useSelector((state) => state.UserSettings?.recordSets?.[setID].recordSetType); + const activitySortColumns = userOfflineMobile ? [] : validActivitySortColumns; const dispatch = useDispatch(); const isTouch = detectTouchDevice(); - - // maybe useful for when there's no headers during dev for adding new types: - /* - const unmappedColumns = mapAndRecordsState?.recordTables?.[props.setID]?.rows[0] - ? Object.keys(mapAndRecordsState?.recordTables?.[props.setID]?.rows[0]) - : []; - */ - const mappedRows = unmappedRows?.map((row) => { - const unnestedRow = tableType === 'Activity' ? getUnnestedFieldsForActivity(row) : getUnnestedFieldsForIAPP(row); + const unnestedRow = + tableType === RecordSetType.Activity ? getUnnestedFieldsForActivity(row) : getUnnestedFieldsForIAPP(row); const mappedRow = {}; Object.keys(unnestedRow).forEach((key) => { mappedRow[key] = unnestedRow[key]; @@ -46,8 +45,8 @@ export const RecordTable = (props) => { return mappedRow; }); - const sortColumn = useSelector((state: any) => state.UserSettings?.recordSets?.[props.setID]?.sortColumn); - const sortOrder = useSelector((state: any) => state.UserSettings?.recordSets?.[props.setID]?.sortOrder); + const sortColumn = useSelector((state: any) => state.UserSettings?.recordSets?.[setID]?.sortColumn); + const sortOrder = useSelector((state: any) => state.UserSettings?.recordSets?.[setID]?.sortOrder); return (
@@ -60,42 +59,40 @@ export const RecordTable = (props) => { )} {tableType === 'Activity' - ? activityColumnsToDisplay.map((col: any, i) => { - return ( - { - if (validActivitySortColumns.includes(col.key)) - dispatch({ type: RECORDSET_SET_SORT, payload: { setID: props.setID, sortColumn: col.key } }); - }} - > - {col.name}{' '} - {validActivitySortColumns.includes(sortColumn) && - sortColumn === col.key && - (sortOrder === 'ASC' ? '▲' : '▼')} - - ); - }) - : iappColumnsToDisplay.map((col: any) => { - return ( - { - if (validIAPPSortColumns.includes(col.key)) - dispatch({ type: RECORDSET_SET_SORT, payload: { setID: props.setID, sortColumn: col.key } }); - }} - > - {col.name}{' '} - {validIAPPSortColumns.includes(sortColumn) && - sortColumn === col.key && - (sortOrder === 'ASC' ? '▲' : '▼')} - - ); - })} + ? activityColumnsToDisplay.map((col: any, i) => ( + { + if (activitySortColumns.includes(col.key)) { + dispatch({ type: RECORDSET_SET_SORT, payload: { setID: setID, sortColumn: col.key } }); + } + }} + > + {col.name}{' '} + {activitySortColumns.includes(sortColumn) && + sortColumn === col.key && + (sortOrder === 'ASC' ? '▲' : '▼')} + + )) + : iappColumnsToDisplay.map((col: any) => ( + { + if (validIAPPSortColumns.includes(col.key)) { + dispatch({ type: RECORDSET_SET_SORT, payload: { setID: setID, sortColumn: col.key } }); + } + }} + > + {col.name}{' '} + {validIAPPSortColumns.includes(sortColumn) && + sortColumn === col.key && + (sortOrder === 'ASC' ? '▲' : '▼')} + + ))} - {mappedRows?.map((row) => { + {mappedRows?.map((row: UserRecord) => { return ( { diff --git a/app/src/UI/Overlay/Records/RecordSet/RecordTableHelpers.tsx b/app/src/UI/Overlay/Records/RecordSet/RecordTableHelpers.tsx index 3d04f6457..45b931d1c 100644 --- a/app/src/UI/Overlay/Records/RecordSet/RecordTableHelpers.tsx +++ b/app/src/UI/Overlay/Records/RecordSet/RecordTableHelpers.tsx @@ -19,18 +19,19 @@ export const getUnnestedFieldsForActivity = (activity) => { }; // needs to be consistent with API column names + const root = activity.hasOwnProperty('activity_payload') ? activity.activity_payload : activity; const columns: any = { activity_id: activity?.activity_id, short_id: activity?.short_id, activity_type: activity?.activity_type, - activity_subtype: ActivitySubtypeShortLabels[activity?.activity_payload?.activity_subtype], - activity_date: new Date(activity?.activity_payload?.form_data?.activity_data?.activity_date_time || null) + activity_subtype: ActivitySubtypeShortLabels[root?.activity_subtype ?? activity?.activity_subtype], + activity_date: new Date( + root?.form_data?.activity_data?.activity_date_time ?? root?.form_data?.activity_data?.activity_date_time ?? null + ) .toISOString() .substring(0, 10), project_code: getArrayString( - Array.isArray(activity?.activity_payload?.form_data?.activity_data?.project_code) - ? activity?.activity_payload?.form_data?.activity_data?.project_code - : [], + Array.isArray(root?.form_data?.activity_data?.project_code) ? root?.form_data?.activity_data?.project_code : [], 'description' ), jurisdiction_display: activity?.jurisdiction_display, @@ -52,11 +53,11 @@ export const getUnnestedFieldsForActivity = (activity) => { biogeoclimatic_zones: activity?.biogeoclimatic_zones, elevation: activity?.elevation, batch_id: activity?.batch_id, - geometry: activity?.activity_payload?.geometry - // date_modified: new Date(activity?.activity_payload?.created_timestamp).toString(), - // reported_area: activity?.activity_payload?.form_data?.activity_data?.reported_area, - // latitude: activity?.activity_payload?.form_data?.activity_data?.latitude, - // longitude: activity?.activity_payload?.form_data?.activity_data?.longitude, + geometry: root?.geometry + // date_modified: new Date(root?.created_timestamp).toString(), + // reported_area: root?.form_data?.activity_data?.reported_area, + // latitude: root?.form_data?.activity_data?.latitude, + // longitude: root?.form_data?.activity_data?.longitude, }; return JSON.parse(JSON.stringify(columns)); diff --git a/app/src/UI/Overlay/Records/Records.css b/app/src/UI/Overlay/Records/Records.css index f31ccaf99..7b398c14b 100644 --- a/app/src/UI/Overlay/Records/Records.css +++ b/app/src/UI/Overlay/Records/Records.css @@ -23,6 +23,8 @@ min-height: 40pt; padding: 5pt 1rem; box-sizing: border-box; + border: none; + background-color: white; border-bottom: 1pt solid black; p { margin: 0; diff --git a/app/src/UI/Overlay/Records/Records.tsx b/app/src/UI/Overlay/Records/Records.tsx index 99b48693c..457501bca 100644 --- a/app/src/UI/Overlay/Records/Records.tsx +++ b/app/src/UI/Overlay/Records/Records.tsx @@ -11,6 +11,8 @@ import { RecordSetType } from 'interfaces/UserRecordSet'; import Prompt from 'state/actions/prompts/Prompt'; import RecordSetDetails from './RecordSetDetails'; import RecordSetControl from './RecordSetControl'; +import { MOBILE } from 'state/build-time-config'; +import filterRecordsetsByNetworkState from 'utils/filterRecordsetsByNetworkState'; export const Records = () => { const DEFAULT_RECORD_TYPES = ['All InvasivesBC Activities', 'All IAPP Records', 'My Drafts']; @@ -19,7 +21,7 @@ export const Records = () => { const mapLayers = useSelector((state) => state.Map.layers); const MapMode = useSelector((state) => state.Map.MapMode); const recordSets = useSelector((state) => state.UserSettings?.recordSets); - + const connected = useSelector((state) => state.Network.connected); const [highlightedSet, setHighlightedSet] = useState(); const [isActivitiesGeoJSONLoaded, setIsActivitiesGeoJSONLoaded] = useState(false); const [loadMap, setLoadMap] = useState({}); @@ -89,7 +91,7 @@ export const Records = () => { const highlightSet = (set: string) => setHighlightedSet(set); const unHighlightSet = () => setHighlightedSet(null); - + const userIsMobileAndOffline = MOBILE && !connected; if (!recordSets) { return; } @@ -98,7 +100,7 @@ export const Records = () => {
    - {Object.keys(recordSets)?.map((set) => ( + {filterRecordsetsByNetworkState(recordSets, userIsMobileAndOffline).map((set) => (
  • history.push('/Records/List/Local:' + set)} @@ -134,20 +136,24 @@ export const Records = () => {
  • ))}
-
- - -
+ {userIsMobileAndOffline ? ( +

Any recordsets that haven't been saved for offline use will not be accessible when you're offline.

+ ) : ( +
+ + +
+ )}
); diff --git a/app/src/interfaces/UserRecord.tsx b/app/src/interfaces/UserRecord.tsx new file mode 100644 index 000000000..4afdfe6f3 --- /dev/null +++ b/app/src/interfaces/UserRecord.tsx @@ -0,0 +1,8 @@ +/** + * Stub interface to reduce later refactoring + */ +interface UserRecord { + [key: string]: any; +} + +export default UserRecord; diff --git a/app/src/interfaces/UserRecordSet.ts b/app/src/interfaces/UserRecordSet.ts index 9a55a6852..2a36cb97b 100644 --- a/app/src/interfaces/UserRecordSet.ts +++ b/app/src/interfaces/UserRecordSet.ts @@ -31,5 +31,6 @@ export interface UserRecordSet { }; cacheMetadata: { status: UserRecordCacheStatus; + idList?: string[]; }; } diff --git a/app/src/state/actions/cache/RecordCache.ts b/app/src/state/actions/cache/RecordCache.ts index 57802cfec..7e872f4fb 100644 --- a/app/src/state/actions/cache/RecordCache.ts +++ b/app/src/state/actions/cache/RecordCache.ts @@ -23,13 +23,14 @@ class RecordCache { const state: RootState = getState() as RootState; - const idsToCache = state.Map.layers.find((l) => l.recordSetID == spec.setId).IDList; + const idsToCache: string[] = state.Map.layers.find((l) => l.recordSetID == spec.setId).IDList; await service.download({ idsToCache, setId: spec.setId, API_BASE: state.Configuration.current.API_BASE }); + return { cachedIds: idsToCache }; } ); } diff --git a/app/src/state/reducers/userSettings.ts b/app/src/state/reducers/userSettings.ts index 559c84715..6743d9faf 100644 --- a/app/src/state/reducers/userSettings.ts +++ b/app/src/state/reducers/userSettings.ts @@ -205,7 +205,8 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState }; } else if (RecordCache.requestCaching.fulfilled.match(action)) { draftState.recordSets[action.meta.arg.setId].cacheMetadata = { - status: UserRecordCacheStatus.CACHED + status: UserRecordCacheStatus.CACHED, + idList: action.payload.cachedIds }; } else if (RecordCache.deleteCache.pending.match(action)) { draftState.recordSets[action.meta.arg.setId].cacheMetadata = { @@ -224,7 +225,8 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState for (const cachedSet of cacheStatus) { if (draftState.recordSets[cachedSet.setId]) { draftState.recordSets[cachedSet.setId].cacheMetadata = { - status: UserRecordCacheStatus.CACHED + status: UserRecordCacheStatus.CACHED, + idList: draftState.recordSets[cachedSet.setId].cacheMetadata.idList ?? [] }; } } diff --git a/app/src/state/sagas/map.ts b/app/src/state/sagas/map.ts index 432457e97..ccf6e3966 100644 --- a/app/src/state/sagas/map.ts +++ b/app/src/state/sagas/map.ts @@ -676,8 +676,10 @@ function* handle_PAGE_OR_LIMIT_UPDATE(action) { const recordSetType = recordSetsState.recordSets?.[action.payload.setID]?.recordSetType; const mapState = yield select(selectMap); - const page = action.payload.page ? action.payload.page : mapState.recordTables?.[action.payload.recordSetID]?.page; - const limit = action.payload.limit + const page = !Number.isNaN(action.payload.page) + ? action.payload.page + : mapState.recordTables?.[action.payload.recordSetID]?.page; + const limit = !Number.isNaN(action.payload.limit) ? action.payload.limit : mapState.recordTables?.[action.payload.recordSetID]?.limit; diff --git a/app/src/state/sagas/map/dataAccess.ts b/app/src/state/sagas/map/dataAccess.ts index 2e81da494..b27539850 100644 --- a/app/src/state/sagas/map/dataAccess.ts +++ b/app/src/state/sagas/map/dataAccess.ts @@ -9,6 +9,7 @@ import { ACTIVITIES_GET_IDS_FOR_RECORDSET_ONLINE, ACTIVITIES_TABLE_ROWS_GET_FAILURE, ACTIVITIES_TABLE_ROWS_GET_ONLINE, + ACTIVITIES_TABLE_ROWS_GET_SUCCESS, ACTIVITY_GET_INITIAL_STATE_FAILURE, FILTERS_PREPPED_FOR_VECTOR_ENDPOINT, IAPP_GEOJSON_GET_ONLINE, @@ -19,6 +20,9 @@ import { import { ACTIVITY_GEOJSON_SOURCE_KEYS, selectMap } from 'state/reducers/map'; import WhatsHere from 'state/actions/whatsHere/WhatsHere'; import { RecordSetType } from 'interfaces/UserRecordSet'; +import { MOBILE } from 'state/build-time-config'; +import { RecordCacheServiceFactory } from 'utils/record-cache/context'; +import GeoShapes from 'constants/geoShapes'; export function* handle_ACTIVITIES_GEOJSON_GET_REQUEST(action) { try { @@ -180,7 +184,11 @@ export function* handle_ACTIVITIES_TABLE_ROWS_GET_REQUEST(action) { try { // new filter object: const currentState = yield select((state) => state.UserSettings); + const connected = yield select((state) => state.Network.connected); const mapState = yield select((state) => state.Map); + + const userMobileOffline = MOBILE && !connected; + const filterObject = getRecordFilterObjectFromStateForAPI( action.payload.recordSetID, currentState, @@ -201,7 +209,24 @@ export function* handle_ACTIVITIES_TABLE_ROWS_GET_REQUEST(action) { return; } - if (true) { + if (userMobileOffline) { + const { recordSetID, page, limit } = action.payload; + const recordSetIdList = yield select( + (state) => state.UserSettings.recordSets[recordSetID].cacheMetadata.idList ?? [] + ); + const service = yield RecordCacheServiceFactory.getPlatformInstance(); + const records = yield service.fetchPaginatedCachedRecords(recordSetIdList, page, limit); + yield put({ + type: ACTIVITIES_TABLE_ROWS_GET_SUCCESS, + payload: { + recordSetID: action.payload.recordSetID, + rows: records, + tableFiltersHash: action.payload.tableFiltersHash, + page: page, + limit: limit + } + }); + } else { yield put({ type: ACTIVITIES_TABLE_ROWS_GET_ONLINE, payload: { @@ -213,9 +238,6 @@ export function* handle_ACTIVITIES_TABLE_ROWS_GET_REQUEST(action) { } }); } - if (false) { - yield put({ type: ACTIVITIES_GEOJSON_GET_OFFLINE, payload: { activityID: action.payload.activityID } }); - } } catch (e) { console.error(e); yield put({ type: ACTIVITIES_TABLE_ROWS_GET_FAILURE }); @@ -315,7 +337,7 @@ export function* handle_MAP_WHATS_HERE_INIT_GET_ACTIVITY(action) { } currentMapState = yield select(selectMap); - const featuresFilteredByShape = []; + const featuresFilteredByShape: Record = []; for (const source of ACTIVITY_GEOJSON_SOURCE_KEYS) { if (!currentMapState?.activitiesGeoJSONDict?.hasOwnProperty(source)) continue; @@ -327,13 +349,13 @@ export function* handle_MAP_WHATS_HERE_INIT_GET_ACTIVITY(action) { ...Object.values(current)?.filter((feature: any) => { const boundaryPolygon = polygon(currentMapState?.whatsHere?.feature?.geometry.coordinates); switch (feature?.geometry?.type) { - case 'Point': + case GeoShapes.Point: const featurePoint = point(feature.geometry.coordinates); return booleanPointInPolygon(featurePoint, boundaryPolygon); - case 'Polygon': + case GeoShapes.Polygon: const featurePolygon = polygon(feature.geometry.coordinates); return intersect(featurePolygon, boundaryPolygon); - case 'MultiPolygon': + case GeoShapes.MultiPolygon: const amultiPolygon = multiPolygon(feature.geometry.coordinates); return intersect(amultiPolygon, boundaryPolygon); default: @@ -347,7 +369,7 @@ export function* handle_MAP_WHATS_HERE_INIT_GET_ACTIVITY(action) { return feature.properties.id; }); - const unfilteredRecordSetIDs = []; + const unfilteredRecordSetIDs: string[] = []; currentMapState?.layers?.map((layer) => { if (layer?.type === 'Activity' && layer?.layerState.mapToggle) { unfilteredRecordSetIDs.push(...layer?.IDList); @@ -367,7 +389,7 @@ export function* handle_MAP_WHATS_HERE_INIT_GET_ACTIVITY(action) { function getSelectColumnsByRecordSetType(recordSetType: any) { //throw new Error('Function not implemented.'); - let columns = []; + let columns: string[] = []; if (recordSetType === 'Activity') { columns = [ 'activity_id', diff --git a/app/src/utils/filterRecordsetsByNetworkState.ts b/app/src/utils/filterRecordsetsByNetworkState.ts new file mode 100644 index 000000000..94dfb070d --- /dev/null +++ b/app/src/utils/filterRecordsetsByNetworkState.ts @@ -0,0 +1,14 @@ +import { UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet'; + +/** + * @desc Filter Recordset keys based on network status of user. Convert Obj keys to array. + * @param recordSets Recordsets cached to users redux state + * @param userOffline Current Network state of user, + * @returns Users accessible recordsets + */ +const filterRecordsetsByNetworkState = (recordSets: Record, userOffline: boolean): string[] => + Object.keys(recordSets).filter((set) => { + return recordSets[set].cacheMetadata.status === UserRecordCacheStatus.CACHED || !userOffline; + }); + +export default filterRecordsetsByNetworkState; diff --git a/app/src/utils/record-cache/index.ts b/app/src/utils/record-cache/index.ts index f99602f5d..40639bc5b 100644 --- a/app/src/utils/record-cache/index.ts +++ b/app/src/utils/record-cache/index.ts @@ -1,3 +1,4 @@ +import UserRecord from 'interfaces/UserRecord'; import { getCurrentJWT } from 'state/sagas/auth/auth'; export interface RecordCacheDownloadRequestSpec { @@ -42,6 +43,8 @@ abstract class RecordCacheService { abstract deleteCachedSet(id: string): Promise; + abstract fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise; + async download( spec: RecordCacheDownloadRequestSpec, progressCallback?: (currentProgress: RecordCacheProgressCallbackParameters) => void diff --git a/app/src/utils/record-cache/localforage-cache.ts b/app/src/utils/record-cache/localforage-cache.ts index bf299815c..0ddede057 100644 --- a/app/src/utils/record-cache/localforage-cache.ts +++ b/app/src/utils/record-cache/localforage-cache.ts @@ -1,10 +1,11 @@ +import UserRecord from 'interfaces/UserRecord'; import localForage from 'localforage'; import { RecordCacheAddSpec, RecordCacheService, RecordSetCacheMetadata } from 'utils/record-cache/index'; class LocalForageRecordCacheService extends RecordCacheService { private static _instance: LocalForageRecordCacheService; - private static CACHED_SETS_METADATA_KEY = 'cached-sets'; + private static readonly CACHED_SETS_METADATA_KEY = 'cached-sets'; private store: LocalForage | null = null; @@ -57,10 +58,33 @@ class LocalForageRecordCacheService extends RecordCacheService { setId: spec.setId, cachedIds: spec.idsToCache }); - await this.store.setItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY, cachedSets); } + /** + * @desc fetch `n` records for a given recordset, supporting pagination + * @param recordSetID Recordset to filter from + * @param page Page to start pagination on + * @param limit Maximum results per page + * @returns { UserRecord[] } Filter Objects + */ + async fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise { + if (recordSetIdList?.length === 0) { + return []; + } + const startPos = page * limit; + const results: any[] = []; + const recordSetLength = recordSetIdList.length; + for (let i = startPos; i < (page + 1) * limit; i++) { + if (i >= recordSetLength) { + break; + } + const entry: UserRecord = (await this.loadActivity(recordSetIdList[i])) as UserRecord; + results.push(entry); + } + return results; + } + async listCachedSets(): Promise { if (this.store == null) { return []; diff --git a/app/src/utils/record-cache/sqlite-cache.ts b/app/src/utils/record-cache/sqlite-cache.ts index a76595c9d..afd13506e 100644 --- a/app/src/utils/record-cache/sqlite-cache.ts +++ b/app/src/utils/record-cache/sqlite-cache.ts @@ -1,4 +1,5 @@ import { SQLiteConnection, SQLiteDBConnection } from '@capacitor-community/sqlite'; +import UserRecord from 'interfaces/UserRecord'; import { RecordCacheAddSpec, RecordCacheService, RecordSetCacheMetadata } from 'utils/record-cache/index'; import { sqlite } from 'utils/sharedSQLiteInstance'; @@ -108,6 +109,45 @@ class SQLiteRecordCacheService extends RecordCacheService { await this.cacheDB.rollbackTransaction(); } } + /** + * @desc fetch `n` records for a given recordset, supporting pagination + * @param recordSetID Recordset to filter from + * @param page Page to start pagination on + * @param limit Maximum results per page + * @returns { UserRecord[] } Filter Objects + */ + async fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise { + if (!recordSetIdList || recordSetIdList.length === 0) { + return []; + } + + const startPos = page * limit; + const results = await this.cacheDB?.query( + // language=SQLite + `SELECT DATA + FROM CACHED_RECORDS + WHERE ID IN (${recordSetIdList.map(() => '?').join(', ')}) + LIMIT ?, ?`, + [...recordSetIdList, startPos, limit] + ); + + if (!results?.values || results.values?.length === 0) { + return []; + } + + const response = results.values + .map((item) => { + try { + return JSON.parse(item['DATA']) as UserRecord; + } catch (e) { + console.error('Error parsing record:', e); + return null; + } + }) + .filter((record) => record !== null); + + return response; + } async listCachedSets(): Promise { if (this.cacheDB == null) {