Skip to content

Commit

Permalink
Merge branch 'dev' into update_ministry_names
Browse files Browse the repository at this point in the history
  • Loading branch information
plasticviking authored Jan 14, 2025
2 parents 5299ef5 + ca5daec commit aa9e054
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 23 deletions.
16 changes: 13 additions & 3 deletions app/src/UI/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
MenuItem,
Switch
} from '@mui/material';
import { useDispatch } from 'react-redux';
import {
AUTH_OPEN_OFFLINE_USER_SELECTION_DIALOG,
AUTH_SIGNIN_REQUEST,
Expand All @@ -39,12 +38,13 @@ import invbclogo from '/assets/InvasivesBC_Icon.svg';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
import { RENDER_DEBUG } from 'UI/App';
import { useSelector } from 'utils/use_selector';
import { AppDispatch, useDispatch, useSelector } from 'utils/use_selector';
import { selectAuth } from 'state/reducers/auth';
import { OfflineSyncHeaderButton } from 'UI/Header/OfflineSyncHeaderButton';
import RefreshButton from './RefreshButton';
import { MOBILE } from 'state/build-time-config';
import NetworkActions from 'state/actions/network/NetworkActions';
import MapActions from 'state/actions/map';

type TabPredicate =
| 'authenticated_any'
Expand Down Expand Up @@ -185,10 +185,20 @@ const LoginButton = ({ labelText = 'Login' }) => {

const LogoutButton = () => {
const dispatch = useDispatch();
const signOutAndTogglePanel = () => {
return (dispatch: AppDispatch) => {
dispatch({
type: TOGGLE_PANEL,
payload: { panelOpen: false }
});
dispatch({ type: AUTH_SIGNOUT_REQUEST });
dispatch(MapActions.toggleOverlay('public_layer'));
};
};
return (
<MenuItem
onClick={() => {
dispatch({ type: AUTH_SIGNOUT_REQUEST });
dispatch(signOutAndTogglePanel());
}}
>
<ListItemIcon>
Expand Down
108 changes: 104 additions & 4 deletions app/src/UI/Overlay/Records/Activity/PhotoContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
Typography
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import { AddAPhoto, DeleteForever } from '@mui/icons-material';
import { PhotoCamera, PhotoLibrary, DeleteForever } from '@mui/icons-material';
import React, { useState } from 'react';
import Activity from 'state/actions/activity/Activity';
import UploadedPhoto from 'interfaces/UploadedPhoto';
import { useDispatch, useSelector } from 'utils/use_selector';
import './PhotoContainer.css';
import Alerts from 'state/actions/alerts/Alerts';
import { AlertSeverity, AlertSubjects } from 'constants/alertEnums';
export interface IPhoto {
file_name: string;
webviewPath?: string;
Expand All @@ -37,8 +39,53 @@ const PhotoContainer: React.FC<IPhotoContainerProps> = (props) => {
const dispatch = useDispatch();
const media = useSelector((state) => state.ActivityPage.activity?.media || []);

const takePhoto = async () => {
async function convertWebPathToDataUrl(webPath: string): Promise<string> {
const response = await fetch(webPath);

// convert response into a blob
const blob = await response.blob();

return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string); // result is a dataUrl
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}

const checkPermissionsAndAlert = async (photoOption: CameraSource): Promise<void> => {
try {
const permissions = await Camera.checkPermissions();

if (photoOption === CameraSource.Camera && permissions.camera === 'denied') {
dispatch(
Alerts.create({
content:
'Camera access is denied. Please enable camera permissions in your device settings to take photos.',
severity: AlertSeverity.Warning,
subject: AlertSubjects.Photo,
autoClose: 5
})
);
} else if (photoOption === CameraSource.Photos && permissions.photos === 'denied') {
dispatch(
Alerts.create({
content:
'Photo library access is denied. Please enable photo library permissions in your device settings to choose photos.',
severity: AlertSeverity.Warning,
subject: AlertSubjects.Photo,
autoClose: 5
})
);
}
} catch (error) {
console.error('Error checking permissions:', error);
}
};

const takePhotoFromCamera = async () => {
try {
await checkPermissionsAndAlert(CameraSource.Camera);
const cameraPhoto = await Camera.getPhoto({
presentationStyle: 'fullscreen',
resultType: CameraResultType.DataUrl,
Expand All @@ -60,6 +107,49 @@ const PhotoContainer: React.FC<IPhotoContainerProps> = (props) => {
}
};

const choosePhotosFromLibrary = async () => {
try {
await checkPermissionsAndAlert(CameraSource.Photos);
const multiplePhotos = await Camera.pickImages({
quality: 100,
limit: 10
});

if (!multiplePhotos.photos.length) {
console.log('No photos selected');
return;
}

// process all photos concurrently
const processedPhotos = await Promise.all(
multiplePhotos.photos.map(async (photo, index) => {
try {
const fileName = `${new Date().getTime()}-${index}.${photo.format}`;
const dataUrl = await convertWebPathToDataUrl(photo.webPath);

return {
file_name: fileName,
encoded_file: dataUrl,
description: 'untitled',
editing: false
} as UploadedPhoto;
} catch (error) {
console.error(`Error processing photo ${index + 1}:`, error);
return null; // skip photo on failure
}
})
);

// filter out failed photo conversions
const validPhotos = processedPhotos.filter((photo) => photo !== null);
validPhotos.forEach((photo) => {
if (photo) dispatch(Activity.Photo.add(photo));
});
} catch (e) {
console.error('error occurred: ', e);
}
};

const deletePhoto = async (photo: UploadedPhoto) => {
dispatch(Activity.Photo.delete(photo));
};
Expand Down Expand Up @@ -129,8 +219,18 @@ const PhotoContainer: React.FC<IPhotoContainerProps> = (props) => {
<Grid container>
<Grid container item spacing={3} justifyContent="center">
<Grid item>
<Button variant="contained" color="primary" startIcon={<AddAPhoto />} onClick={takePhoto}>
Add A Photo
<Button variant="contained" color="primary" startIcon={<PhotoCamera />} onClick={takePhotoFromCamera}>
Capture Photo
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
startIcon={<PhotoLibrary />}
onClick={choosePhotosFromLibrary}
>
Choose from Gallery
</Button>
</Grid>
</Grid>
Expand Down
14 changes: 7 additions & 7 deletions app/src/UI/Overlay/Records/RecordSet/RecordTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => {
View/Edit
</th>
)}
{tableType === 'Activity'
? activityColumnsToDisplay.map((col: any, i) => (
{tableType === RecordSetType.Activity
? activityColumnsToDisplay.map((col: any) => (
<th
className={'record_table_header_column'}
key={col.key}
Expand Down Expand Up @@ -106,7 +106,7 @@ export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => {
type: USER_CLICKED_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
}
});
Expand All @@ -119,13 +119,13 @@ export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => {
type: USER_TOUCHED_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
}
});
}}
className="record_table_row"
key={row?.activity_id}
key={row?.activity_id ?? row?.site_id}
>
{isTouch && (
<td
Expand All @@ -134,7 +134,7 @@ export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => {
type: USER_CLICKED_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
}
});
Expand All @@ -145,7 +145,7 @@ export const RecordTable = ({ setID, userOfflineMobile }: PropTypes) => {
<VisibilityIcon />
</td>
)}
{tableType === 'Activity'
{tableType === RecordSetType.Activity
? activityColumnsToDisplay.map((col) => {
return (
<td className="record_table_row_column" key={col.key + col.name}>
Expand Down
2 changes: 1 addition & 1 deletion app/src/constants/offline_state_version.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
handle purging the offline storage on app version upgrade
*/
export const CURRENT_MIGRATION_VERSION = 20241118;
export const CURRENT_MIGRATION_VERSION = 20250113;
export const MIGRATION_VERSION_KEY = '_persistedMigrationVersion';
2 changes: 1 addition & 1 deletion app/src/state/actions/userSettings/RecordSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class RecordSet {
static readonly removeFilter = createAction<IRemoveFilter>(`${this.PREFIX}/removeFilter`);

private static readonly createDefaultRecordset = (type: RecordSetType): UserRecordSet => ({
tableFilters: null,
tableFilters: [],
id: nanoid(),
color: RECORD_COLOURS[0],
drawOrder: 0,
Expand Down
9 changes: 4 additions & 5 deletions app/src/state/sagas/map/dataAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from 'state/actions';
import { ACTIVITY_GEOJSON_SOURCE_KEYS, selectMap } from 'state/reducers/map';
import WhatsHere from 'state/actions/whatsHere/WhatsHere';
import { RecordSetType } from 'interfaces/UserRecordSet';
import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet';
import { MOBILE } from 'state/build-time-config';
import { RecordCacheServiceFactory } from 'utils/record-cache/context';
import GeoShapes from 'constants/geoShapes';
Expand Down Expand Up @@ -60,7 +60,7 @@ export function* handle_PREP_FILTERS_FOR_VECTOR_ENDPOINT(action) {
try {
const currentState = yield select((state) => state?.UserSettings);
const clientBoundaries = yield select((state) => state.Map?.clientBoundaries);
const cacheMetadata = currentState.recordSets[action.payload.recordSetID].cacheMetadata;
const recordset: UserRecordSet = currentState.recordSets[action.payload.recordSetID];
const filterObject = getRecordFilterObjectFromStateForAPI(
action.payload.recordSetID,
currentState,
Expand All @@ -83,8 +83,8 @@ export function* handle_PREP_FILTERS_FOR_VECTOR_ENDPOINT(action) {
filterObject: filterObject,
recordSetID: action.payload.recordSetID,
tableFiltersHash: action.payload.tableFiltersHash,
recordSetType: action.payload.recordSetType,
cacheMetadata: cacheMetadata ?? null
recordSetType: recordset.recordSetType,
cacheMetadata: recordset.cacheMetadata ?? null
}
});
} catch (e) {
Expand Down Expand Up @@ -404,7 +404,6 @@ export function* handle_MAP_WHATS_HERE_INIT_GET_ACTIVITY(action) {
}

export function getSelectColumnsByRecordSetType(recordSetType: any) {
//throw new Error('Function not implemented.');
let columns: string[] = [];
if (recordSetType === 'Activity') {
columns = [
Expand Down
4 changes: 2 additions & 2 deletions app/src/utils/closestWellsHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import polygonToLine from '@turf/polygon-to-line';
import inside from '@turf/inside';
import buffer from '@turf/buffer';
import { getDataFromDataBCv2 } from './WFSConsumer';
import { selectNetworkConnected } from 'state/reducers/network';
import { selectNetworkState } from 'state/reducers/network';
import { select } from 'redux-saga/effects';

//gets layer data based on the layer name
export function* getClosestWells(inputGeometry) {
const firstFeature = inputGeometry;
const networkState = yield select(selectNetworkConnected);
const networkState = yield select(selectNetworkState);
//get the map extent as geoJson polygon feature
const bufferedGeo = buffer(firstFeature, 1, { units: 'kilometers' });
//if well layer is selected
Expand Down
28 changes: 28 additions & 0 deletions database/src/migrations/0030_add_crd_ipmas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Knex } from 'knex';
import axios from 'axios';
import { ungzip } from 'node-gzip';

export async function up(knex: Knex): Promise<void> {
try {
const url = 'https://nrs.objectstore.gov.bc.ca/seeds/CRD_IPMAS.sql.gz';
const { data } = await axios.get(url, { responseType: 'arraybuffer' });
const sql = await ungzip(data);

await knex.raw(sql.toString());
} catch (e) {
console.error('Failed to insert IPMAS data:', e);
throw e;
}
}

export async function down(knex: Knex): Promise<void> {
try {
await knex.raw(`
DELETE FROM public.invasive_plant_management_areas
WHERE agency_cd = 'CRD';
`);
} catch (e) {
console.error('Failed to rollback IPMAS data:', e);
throw e;
}
}
28 changes: 28 additions & 0 deletions database/src/migrations/0031_add_nrrm_to_regional_districts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Knex } from 'knex';
import axios from 'axios';
import { ungzip } from 'node-gzip';

export async function up(knex: Knex): Promise<void> {
try {
const url = 'https://nrs.objectstore.gov.bc.ca/seeds/NRRM.sql.gz';
const { data } = await axios.get(url, { responseType: 'arraybuffer' });
const sql = await ungzip(data);

await knex.raw(sql.toString());
} catch (e) {
console.error('Failed to insert NRRM data:', e);
throw e;
}
}

export async function down(knex: Knex): Promise<void> {
try {
await knex.raw(`
DELETE FROM public.regional_districts
WHERE agency_cd = 'NRRM';
`);
} catch (e) {
console.error('Failed to rollback NRRM data:', e);
throw e;
}
}

0 comments on commit aa9e054

Please sign in to comment.