From e976b831438724a3acae793e8a0588b46dc7872e Mon Sep 17 00:00:00 2001 From: Al Rosenthal Date: Wed, 11 Oct 2023 12:25:30 -0700 Subject: [PATCH] SIMSBIOHUB-288: Sample Site Dropdowns (#1110) * Add datagrid autocomplete components * Add datagrid support for sample site, method, and period columns * Add time component to data grid. * Added node-pg float type parser * Save/Edit datagrid observations working --- api/package-lock.json | 12 +- api/src/database/db.ts | 5 + .../paths/project/{projectId}/survey/list.ts | 2 +- .../{surveyId}/observation/index.test.ts | 24 +- .../survey/{surveyId}/observation/index.ts | 39 ++- .../survey/{surveyId}/sample-site/index.ts | 2 + .../sample-site/{surveySampleSiteId}/index.ts | 2 + .../sample-method/index.ts | 2 + .../{surveySampleMethodId}/index.ts | 2 + .../sample-period/index.ts | 2 + .../sample-period/{surveySamplePeriodId}.ts | 2 + .../repositories/observation-repository.ts | 47 ++- .../sample-location-repository.ts | 52 ++- .../repositories/sample-method-repository.ts | 5 +- api/src/services/observation-service.test.ts | 20 +- .../services/sample-location-service.test.ts | 12 +- .../services/sample-method-service.test.ts | 12 +- app/package-lock.json | 254 +++++++------- .../AsyncAutocompleteDataGridEditCell.tsx | 204 +++++++++++ .../AutocompleteDataGrid.interface.ts | 11 + .../AutocompleteDataGridEditCell.tsx | 121 +++++++ .../AutocompleteDataGridViewCell.tsx | 39 +++ ...onditionalAutocompleteDataGridEditCell.tsx | 62 ++++ ...onditionalAutocompleteDataGridViewCell.tsx | 61 ++++ .../taxonomy/TaxonomyDataGridEditCell.tsx | 67 ++++ .../taxonomy/TaxonomyDataGridViewCell.tsx | 38 ++ app/src/contexts/observationsContext.tsx | 22 +- app/src/contexts/surveyContext.tsx | 30 +- .../components/CreateFundingSource.tsx | 4 +- .../components/EditFundingSource.tsx | 4 +- app/src/features/surveys/SurveyRouter.tsx | 8 + .../surveys/components/SamplingMethodForm.tsx | 21 +- .../components/StratumCreateOrEditDialog.tsx | 3 +- .../observations/ObservationComponent.tsx | 54 ++- .../observations/ObservationsTable.tsx | 228 ++++++++++-- .../observations/SurveyObservationHeader.tsx | 8 +- .../sampling-sites/SamplingSiteHeader.tsx | 31 +- .../sampling-sites/SamplingSiteList.tsx | 328 ++++++++++-------- .../sampling-sites/SamplingSitePage.tsx | 10 +- .../SampleSiteFileUploadItemSubtext.tsx | 2 +- .../edit/SamplingSiteEditPage.tsx | 144 ++++++++ .../edit/components/SampleSiteEditForm.tsx | 136 ++++++++ .../SampleSiteGeneralInformationForm.tsx | 35 ++ .../surveys/view/SurveyDetails.test.tsx | 4 +- .../surveys/view/SurveyHeader.test.tsx | 3 + .../SurveyGeneralInformation.test.tsx | 12 +- .../components/SurveyProprietaryData.test.tsx | 12 +- .../SurveyPurposeAndMethodologyData.test.tsx | 8 +- .../view/components/SurveyStudyArea.test.tsx | 20 +- .../SurveyObservations.test.tsx | 1 + app/src/hooks/api/useSamplingSiteApi.ts | 42 ++- app/src/interfaces/useSurveyApi.interface.ts | 43 +++ ...230929134200_update_survey_observations.ts | 192 ++++++++++ 53 files changed, 2067 insertions(+), 437 deletions(-) create mode 100644 app/src/components/data-grid/autocomplete/AsyncAutocompleteDataGridEditCell.tsx create mode 100644 app/src/components/data-grid/autocomplete/AutocompleteDataGrid.interface.ts create mode 100644 app/src/components/data-grid/autocomplete/AutocompleteDataGridEditCell.tsx create mode 100644 app/src/components/data-grid/autocomplete/AutocompleteDataGridViewCell.tsx create mode 100644 app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridEditCell.tsx create mode 100644 app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridViewCell.tsx create mode 100644 app/src/components/data-grid/taxonomy/TaxonomyDataGridEditCell.tsx create mode 100644 app/src/components/data-grid/taxonomy/TaxonomyDataGridViewCell.tsx create mode 100644 app/src/features/surveys/observations/sampling-sites/edit/SamplingSiteEditPage.tsx create mode 100644 app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteEditForm.tsx create mode 100644 app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteGeneralInformationForm.tsx create mode 100644 database/src/migrations/20230929134200_update_survey_observations.ts diff --git a/api/package-lock.json b/api/package-lock.json index 35ff7ff65d..0cab9949f9 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -2965,7 +2965,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.4", @@ -3097,7 +3097,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -3371,7 +3371,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "events": { "version": "1.1.1", @@ -4003,7 +4003,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fromentries": { "version": "1.3.2", @@ -5030,7 +5030,7 @@ "is-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", - "integrity": "sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ==" + "integrity": "sha1-QdN/SV/MrMBaR3jWboMCTCkro/8=" }, "is-extendable": { "version": "0.1.1", @@ -9094,7 +9094,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", diff --git a/api/src/database/db.ts b/api/src/database/db.ts index ca5845a979..8b0dc8a200 100644 --- a/api/src/database/db.ts +++ b/api/src/database/db.ts @@ -51,6 +51,11 @@ pg.types.setTypeParser(pg.types.builtins.TIMESTAMP, (stringValue: string) => { pg.types.setTypeParser(pg.types.builtins.TIMESTAMPTZ, (stringValue: string) => { return stringValue; // 1082 for `DATE` type }); +// NUMERIC column types return as strings to maintain precision. Converting this to a float so it is usable by the system +// Explanation of why Numeric returns as a string: https://github.com/brianc/node-postgres/issues/811 +pg.types.setTypeParser(pg.types.builtins.NUMERIC, (stringValue: string) => { + return parseFloat(stringValue); +}); // singleton pg pool instance used by the api let DBPool: pg.Pool | undefined; diff --git a/api/src/paths/project/{projectId}/survey/list.ts b/api/src/paths/project/{projectId}/survey/list.ts index 64768c4760..c585548b4a 100644 --- a/api/src/paths/project/{projectId}/survey/list.ts +++ b/api/src/paths/project/{projectId}/survey/list.ts @@ -8,7 +8,7 @@ import { authorizeRequestHandler } from '../../../../request-handlers/security/a import { SurveyService } from '../../../../services/survey-service'; import { getLogger } from '../../../../utils/logger'; -const defaultLog = getLogger('paths/project/{projectId}/surveys'); +const defaultLog = getLogger('paths/project/{projectId}/survey/list'); export const GET: Operation = [ authorizeRequestHandler((req) => { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.test.ts index f823d9fdb9..24a4cad381 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.test.ts @@ -462,7 +462,10 @@ describe('insertUpdateSurveyObservations', () => { latitude: 48.103322, longitude: -122.798892, observation_date: '1970-01-01', - observation_time: '00:00:00' + observation_time: '00:00:00', + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 }, { wldtaxonomic_units_id: 1234, @@ -470,12 +473,15 @@ describe('insertUpdateSurveyObservations', () => { latitude: 48.103322, longitude: -122.798892, observation_date: '1970-01-01', - observation_time: '00:00:00' + observation_time: '00:00:00', + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 } ] }; - const requestHandler = observationRecords.insertUpdateSurveyObservations(); + const requestHandler = observationRecords.insertUpdateDeleteSurveyObservations(); await requestHandler(mockReq, mockRes, mockNext); expect(insertUpdateSurveyObservationsStub).to.have.been.calledOnceWith(2, [ @@ -486,7 +492,10 @@ describe('insertUpdateSurveyObservations', () => { longitude: -122.798892, count: 99, observation_date: '1970-01-01', - observation_time: '00:00:00' + observation_time: '00:00:00', + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 }, { survey_observation_id: undefined, @@ -495,7 +504,10 @@ describe('insertUpdateSurveyObservations', () => { longitude: -122.798892, count: 99, observation_date: '1970-01-01', - observation_time: '00:00:00' + observation_time: '00:00:00', + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 } ]); expect(mockRes.statusValue).to.equal(200); @@ -533,7 +545,7 @@ describe('insertUpdateSurveyObservations', () => { }; try { - const requestHandler = observationRecords.insertUpdateSurveyObservations(); + const requestHandler = observationRecords.insertUpdateDeleteSurveyObservations(); await requestHandler(mockReq, mockRes, mockNext); expect.fail(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.ts index 1ce826c4f8..3a3dbd1683 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/index.ts @@ -49,7 +49,7 @@ export const PUT: Operation = [ ] }; }), - insertUpdateSurveyObservations() + insertUpdateDeleteSurveyObservations() ]; const surveyObservationsResponseSchema: SchemaObject = { @@ -127,7 +127,7 @@ const surveyObservationsResponseSchema: SchemaObject = { }; GET.apiDoc = { - description: 'Fetches observation records for the given survey.', + description: 'Get all observations for the survey.', tags: ['observation'], security: [ { @@ -170,7 +170,7 @@ GET.apiDoc = { $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -182,8 +182,8 @@ GET.apiDoc = { }; PUT.apiDoc = { - description: 'Fetches observation records for the given survey.', - tags: ['attachments'], + description: 'Insert/update/delete observations for the survey.', + tags: ['observation'], security: [ { Bearer: [] @@ -250,7 +250,7 @@ PUT.apiDoc = { }, responses: { 200: { - description: 'Upload OK', + description: 'Update OK', content: { 'application/json': { schema: { ...surveyObservationsResponseSchema } @@ -264,7 +264,7 @@ PUT.apiDoc = { $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -275,6 +275,12 @@ PUT.apiDoc = { } }; +/** + * Fetch all observations for a survey. + * + * @export + * @return {*} {RequestHandler} + */ export function getSurveyObservations(): RequestHandler { return async (req, res) => { const surveyId = Number(req.params.surveyId); @@ -300,11 +306,19 @@ export function getSurveyObservations(): RequestHandler { }; } -export function insertUpdateSurveyObservations(): RequestHandler { +/** + * Inserts new observation records. + * Updates existing observation records. + * Deletes missing observation records. + * + * @export + * @return {*} {RequestHandler} + */ +export function insertUpdateDeleteSurveyObservations(): RequestHandler { return async (req, res) => { const surveyId = Number(req.params.surveyId); - defaultLog.debug({ label: 'insertUpdateSurveyObservations', surveyId }); + defaultLog.debug({ label: 'insertUpdateDeleteSurveyObservations', surveyId }); const connection = getDBConnection(req['keycloak_token']); @@ -318,12 +332,15 @@ export function insertUpdateSurveyObservations(): RequestHandler { return { survey_observation_id: record.survey_observation_id, wldtaxonomic_units_id: Number(record.wldtaxonomic_units_id), + survey_sample_site_id: record.survey_sample_site_id, + survey_sample_method_id: record.survey_sample_method_id, + survey_sample_period_id: record.survey_sample_period_id, latitude: record.latitude, longitude: record.longitude, count: record.count, observation_date: record.observation_date, observation_time: record.observation_time - }; + } as InsertObservation | UpdateObservation; }); const surveyObservations = await observationService.insertUpdateDeleteSurveyObservations(surveyId, records); @@ -332,7 +349,7 @@ export function insertUpdateSurveyObservations(): RequestHandler { return res.status(200).json({ surveyObservations }); } catch (error) { - defaultLog.error({ label: 'insertUpdateSurveyObservations', message: 'error', error }); + defaultLog.error({ label: 'insertUpdateDeleteSurveyObservations', message: 'error', error }); await connection.rollback(); throw error; } finally { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/index.ts index 90ea21caf3..25b2dc89fa 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/index.ts @@ -158,6 +158,7 @@ export function getSurveySampleLocationRecords(): RequestHandler { return res.status(200).json({ sampleSites: result }); } catch (error) { defaultLog.error({ label: 'getSurveySampleLocationRecords', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -309,6 +310,7 @@ export function createSurveySampleSiteRecord(): RequestHandler { return res.status(201).send(); } catch (error) { defaultLog.error({ label: 'insertProjectParticipants', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts index d0e21d91f5..450b0d1a08 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/index.ts @@ -146,6 +146,7 @@ export function updateSurveySampleSite(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'updateSurveySampleSite', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -253,6 +254,7 @@ export function deleteSurveySampleSiteRecord(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'deleteSurveySampleSiteRecord', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/index.ts index 9659e7c79d..0a98d5de6d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/index.ts @@ -163,6 +163,7 @@ export function getSurveySampleMethodRecords(): RequestHandler { return res.status(200).json({ sampleMethods: result }); } catch (error) { defaultLog.error({ label: 'getSurveySampleMethodRecords', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -298,6 +299,7 @@ export function createSurveySampleSiteRecord(): RequestHandler { return res.status(201).send(); } catch (error) { defaultLog.error({ label: 'insertProjectParticipants', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/index.ts index 0f47062b8f..a1b3e1325f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/index.ts @@ -143,6 +143,7 @@ export function updateSurveySampleMethod(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'updateSurveySampleMethod', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -250,6 +251,7 @@ export function deleteSurveySampleMethodRecord(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'deleteSurveySampleMethodRecord', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/index.ts index c242dae997..2410e8ab3e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/index.ts @@ -172,6 +172,7 @@ export function getSurveySamplePeriodRecords(): RequestHandler { return res.status(200).json({ samplePeriods: result }); } catch (error) { defaultLog.error({ label: 'getSurveySamplePeriodRecords', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -311,6 +312,7 @@ export function createSurveySamplePeriodRecord(): RequestHandler { return res.status(201).send(); } catch (error) { defaultLog.error({ label: 'createSurveySamplePeriodRecord', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/{surveySamplePeriodId}.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/{surveySamplePeriodId}.ts index e8ac2d7b4a..5a6a13b6f4 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/{surveySamplePeriodId}.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/{surveySampleSiteId}/sample-method/{surveySampleMethodId}/sample-period/{surveySamplePeriodId}.ts @@ -162,6 +162,7 @@ export function updateSurveySamplePeriod(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'updateSurveySamplePeriod', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); @@ -287,6 +288,7 @@ export function deleteSurveySamplePeriodRecord(): RequestHandler { return res.status(204).send(); } catch (error) { defaultLog.error({ label: 'deleteSurveySamplePeriodRecord', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index ddd3a94a42..8843051a42 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -1,4 +1,3 @@ -import moment from 'moment'; import SQL from 'sql-template-strings'; import { z } from 'zod'; import { getKnex } from '../database/db'; @@ -11,6 +10,9 @@ export const ObservationRecord = z.object({ survey_observation_id: z.number(), survey_id: z.number(), wldtaxonomic_units_id: z.number(), + survey_sample_site_id: z.number(), + survey_sample_method_id: z.number(), + survey_sample_period_id: z.number(), latitude: z.number(), longitude: z.number(), count: z.number(), @@ -30,7 +32,16 @@ export type ObservationRecord = z.infer; */ export type InsertObservation = Pick< ObservationRecord, - 'survey_id' | 'wldtaxonomic_units_id' | 'latitude' | 'longitude' | 'count' | 'observation_date' | 'observation_time' + | 'survey_id' + | 'wldtaxonomic_units_id' + | 'latitude' + | 'longitude' + | 'count' + | 'observation_date' + | 'observation_time' + | 'survey_sample_site_id' + | 'survey_sample_method_id' + | 'survey_sample_period_id' >; /** @@ -45,6 +56,9 @@ export type UpdateObservation = Pick< | 'count' | 'observation_date' | 'observation_time' + | 'survey_sample_site_id' + | 'survey_sample_method_id' + | 'survey_sample_period_id' >; export class ObservationRepository extends BaseRepository { @@ -95,6 +109,10 @@ export class ObservationRepository extends BaseRepository { surveyId: number, observations: (InsertObservation | UpdateObservation)[] ): Promise { + if (!observations.length) { + // no observations to create or update, leave early + return []; + } const sqlStatement = SQL` INSERT INTO survey_observation @@ -102,6 +120,9 @@ export class ObservationRepository extends BaseRepository { survey_observation_id, survey_id, wldtaxonomic_units_id, + survey_sample_site_id, + survey_sample_method_id, + survey_sample_period_id, count, latitude, longitude, @@ -119,10 +140,13 @@ export class ObservationRepository extends BaseRepository { observation['survey_observation_id'] || 'DEFAULT', surveyId, observation.wldtaxonomic_units_id, + observation.survey_sample_site_id, + observation.survey_sample_method_id, + observation.survey_sample_period_id, observation.count, observation.latitude, observation.longitude, - `'${moment(observation.observation_date).format('YYYY-MM-DD')}'`, + `'${observation.observation_date}'`, `'${observation.observation_time}'` ].join(', ')})`; }) @@ -134,6 +158,9 @@ export class ObservationRepository extends BaseRepository { (survey_observation_id) DO UPDATE SET wldtaxonomic_units_id = EXCLUDED.wldtaxonomic_units_id, + survey_sample_site_id = EXCLUDED.survey_sample_site_id, + survey_sample_method_id = EXCLUDED.survey_sample_method_id, + survey_sample_period_id = EXCLUDED.survey_sample_period_id, count = EXCLUDED.count, observation_date = EXCLUDED.observation_date, observation_time = EXCLUDED.observation_time, @@ -142,12 +169,8 @@ export class ObservationRepository extends BaseRepository { `); sqlStatement.append(` - RETURNING - *, - latitude::double precision, - longitude::double precision - ;`); - + RETURNING*; + `); const response = await this.connection.sql(sqlStatement, ObservationRecord); return response.rows; @@ -162,11 +185,7 @@ export class ObservationRepository extends BaseRepository { */ async getSurveyObservations(surveyId: number): Promise { const knex = getKnex(); - const sqlStatement = knex - .queryBuilder() - .select('*', knex.raw('latitude::double precision'), knex.raw('longitude::double precision')) - .from('survey_observation') - .where('survey_id', surveyId); + const sqlStatement = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId); const response = await this.connection.knex(sqlStatement, ObservationRecord); return response.rows; diff --git a/api/src/repositories/sample-location-repository.ts b/api/src/repositories/sample-location-repository.ts index 598506adfa..44c6d9f561 100644 --- a/api/src/repositories/sample-location-repository.ts +++ b/api/src/repositories/sample-location-repository.ts @@ -1,8 +1,10 @@ import SQL from 'sql-template-strings'; import { z } from 'zod'; +import { getKnex } from '../database/db'; import { ApiExecuteSQLError } from '../errors/api-error'; import { generateGeometryCollectionSQL } from '../utils/spatial-utils'; import { BaseRepository } from './base-repository'; +import { SampleMethodRecord } from './sample-method-repository'; // This describes a row in the database for Survey Sample Location export const SampleLocationRecord = z.object({ @@ -16,7 +18,8 @@ export const SampleLocationRecord = z.object({ create_user: z.number(), update_date: z.string().nullable(), update_user: z.number().nullable(), - revision_count: z.number() + revision_count: z.number(), + sample_methods: z.array(SampleMethodRecord).default([]) }); export type SampleLocationRecord = z.infer; @@ -45,13 +48,46 @@ export class SampleLocationRepository extends BaseRepository { * @memberof SampleLocationRepository */ async getSampleLocationsForSurveyId(surveyId: number): Promise { - const sql = SQL` - SELECT * - FROM survey_sample_site - WHERE survey_id = ${surveyId}; - `; - - const response = await this.connection.sql(sql, SampleLocationRecord); + const knex = getKnex(); + const queryBuilder = knex + .queryBuilder() + .with('json_sample_period', (qb) => { + // aggregate all sample periods based on method id + qb.select('survey_sample_method_id', knex.raw('json_agg(ssp.*) as sample_periods')) + .from({ ssp: 'survey_sample_period' }) + .groupBy('survey_sample_method_id'); + }) + .with('json_sample_methods', (qb) => { + // join aggregated samples to methods + // aggregate methods base on site id + qb.select( + 'survey_sample_site_id', + knex.raw(` + json_agg(json_build_object( + 'sample_periods', jsp.sample_periods, + 'survey_sample_method_id', ssm.survey_sample_method_id, + 'method_lookup_id', ssm.method_lookup_id, + 'description', ssm.description, + 'create_date', ssm.create_date, + 'create_user', ssm.create_user, + 'update_date', ssm.update_date, + 'update_user', ssm.update_user, + 'survey_sample_site_id', ssm.survey_sample_site_id, + 'revision_count', ssm.revision_count + )) as sample_methods`) + ) + .from({ ssm: 'survey_sample_method' }) + .leftJoin('json_sample_period as jsp', 'jsp.survey_sample_method_id', 'ssm.survey_sample_method_id') + .groupBy('ssm.survey_sample_site_id'); + }) + // join aggregated methods to sampling sites + .select('*') + .from({ sss: 'survey_sample_site' }) + .leftJoin('json_sample_methods as jsm', 'jsm.survey_sample_site_id', 'sss.survey_sample_site_id') + .where('sss.survey_id', surveyId) + .orderBy('sss.survey_sample_site_id', 'asc'); + + const response = await this.connection.knex(queryBuilder, SampleLocationRecord); return response.rows; } diff --git a/api/src/repositories/sample-method-repository.ts b/api/src/repositories/sample-method-repository.ts index 53689ce433..62348546ab 100644 --- a/api/src/repositories/sample-method-repository.ts +++ b/api/src/repositories/sample-method-repository.ts @@ -2,7 +2,7 @@ import SQL from 'sql-template-strings'; import { z } from 'zod'; import { ApiExecuteSQLError } from '../errors/api-error'; import { BaseRepository } from './base-repository'; -import { InsertSamplePeriodRecord, UpdateSamplePeriodRecord } from './sample-period-repository'; +import { InsertSamplePeriodRecord, SamplePeriodRecord, UpdateSamplePeriodRecord } from './sample-period-repository'; export type InsertSampleMethodRecord = Pick< SampleMethodRecord, @@ -24,7 +24,8 @@ export const SampleMethodRecord = z.object({ create_user: z.number(), update_date: z.string().nullable(), update_user: z.number().nullable(), - revision_count: z.number() + revision_count: z.number(), + sample_periods: z.array(SamplePeriodRecord).default([]) }); export type SampleMethodRecord = z.infer; diff --git a/api/src/services/observation-service.test.ts b/api/src/services/observation-service.test.ts index 7524b09531..58ce16e23f 100644 --- a/api/src/services/observation-service.test.ts +++ b/api/src/services/observation-service.test.ts @@ -47,7 +47,10 @@ describe('ObservationService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 }, { survey_observation_id: 6, @@ -62,7 +65,10 @@ describe('ObservationService', () => { create_user: 1, update_date: '2023-04-04', update_user: 2, - revision_count: 1 + revision_count: 1, + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 } ]; const insertUpdateSurveyObservationsStub = sinon @@ -119,7 +125,10 @@ describe('ObservationService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 }, { survey_observation_id: 6, @@ -134,7 +143,10 @@ describe('ObservationService', () => { create_user: 1, update_date: '2023-04-04', update_user: 2, - revision_count: 1 + revision_count: 1, + survey_sample_site_id: 1, + survey_sample_method_id: 1, + survey_sample_period_id: 1 } ]; const getSurveyObservationsStub = sinon diff --git a/api/src/services/sample-location-service.test.ts b/api/src/services/sample-location-service.test.ts index cdf2550e01..12855d9b03 100644 --- a/api/src/services/sample-location-service.test.ts +++ b/api/src/services/sample-location-service.test.ts @@ -68,7 +68,8 @@ describe('SampleLocationService', () => { create_user: 1, update_date: '', update_user: 1, - revision_count: 0 + revision_count: 0, + sample_methods: [] }); const insertMethod = sinon.stub(SampleMethodService.prototype, 'insertSampleMethod').resolves(); @@ -96,7 +97,8 @@ describe('SampleLocationService', () => { create_user: 1, update_date: '', update_user: 1, - revision_count: 0 + revision_count: 0, + sample_methods: [] } ]); @@ -125,7 +127,8 @@ describe('SampleLocationService', () => { create_user: 1, update_date: '', update_user: 1, - revision_count: 0 + revision_count: 0, + sample_methods: [] }); const { survey_sample_site_id } = await service.deleteSampleLocationRecord(1); @@ -150,7 +153,8 @@ describe('SampleLocationService', () => { create_user: 1, update_date: '', update_user: 1, - revision_count: 0 + revision_count: 0, + sample_methods: [] }); const { name, description } = await service.updateSampleLocation({ diff --git a/api/src/services/sample-method-service.test.ts b/api/src/services/sample-method-service.test.ts index c5ad6659ff..7c30fac38e 100644 --- a/api/src/services/sample-method-service.test.ts +++ b/api/src/services/sample-method-service.test.ts @@ -41,7 +41,8 @@ describe('SampleMethodService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + sample_periods: [] } ]; const getSampleMethodsForSurveySampleSiteIdStub = sinon @@ -74,7 +75,8 @@ describe('SampleMethodService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + sample_periods: [] }; const deleteSampleMethodRecordStub = sinon .stub(SampleMethodRepository.prototype, 'deleteSampleMethodRecord') @@ -106,7 +108,8 @@ describe('SampleMethodService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + sample_periods: [] }; const insertSampleMethodStub = sinon .stub(SampleMethodRepository.prototype, 'insertSampleMethod') @@ -171,7 +174,8 @@ describe('SampleMethodService', () => { create_user: 1, update_date: null, update_user: null, - revision_count: 0 + revision_count: 0, + sample_periods: [] }; const updateSampleMethodStub = sinon .stub(SampleMethodRepository.prototype, 'updateSampleMethod') diff --git a/app/package-lock.json b/app/package-lock.json index 9486910036..320cefe5b6 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2664,13 +2664,13 @@ } }, "@mui/x-data-grid-pro": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid-pro/-/x-data-grid-pro-6.14.0.tgz", - "integrity": "sha512-7k9F97g4M9klWzqHhcoGLzrNk+JLpNKRL+/g/4nR7IbrlyGkw8FhUTppM8UAOg2mDTCtjlRj0y9iR2C5p22lyQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid-pro/-/x-data-grid-pro-6.15.0.tgz", + "integrity": "sha512-4YUWKCN8o+ZjcJO6FpdhnuXqrxrR0AYgr5pUaUaGyurvcpMZdxL1SwYjzSZyzL0NRMU/tK2DAWS77vHl89xSFA==", "requires": { "@babel/runtime": "^7.22.15", "@mui/utils": "^5.14.8", - "@mui/x-data-grid": "6.14.0", + "@mui/x-data-grid": "6.15.0", "@mui/x-license-pro": "6.10.2", "@types/format-util": "^1.0.2", "clsx": "^2.0.0", @@ -2679,17 +2679,17 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", "requires": { "regenerator-runtime": "^0.14.0" } }, "@mui/utils": { - "version": "5.14.10", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.10.tgz", - "integrity": "sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.11.tgz", + "integrity": "sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==", "requires": { "@babel/runtime": "^7.22.15", "@types/prop-types": "^15.7.5", @@ -2698,9 +2698,9 @@ } }, "@mui/x-data-grid": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.14.0.tgz", - "integrity": "sha512-EMkPT0YQsjqfH8f/UpPscpPFlywWuyrkS6aaB90t821Z6khoheFS1XeKbCa3L6byC/fwt1cAmIljlh8xJxIueg==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.15.0.tgz", + "integrity": "sha512-0u7Z+al7M1BOni5SILp+gc4coJy88oOrUoVfOSGQdfTAJwcoHvP3KZYA80MGBsQwo5A9/bZz40HK8+mdm+ftnA==", "requires": { "@babel/runtime": "^7.22.15", "@mui/utils": "^5.14.8", @@ -4555,7 +4555,7 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ansi-escapes": { "version": "4.3.2", @@ -4657,12 +4657,12 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==" + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { "version": "3.1.6", @@ -4777,7 +4777,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "ast-types-flow": { "version": "0.0.7", @@ -4794,12 +4794,12 @@ "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==" + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "at-least-node": { "version": "1.0.0", @@ -4835,7 +4835,7 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.12.0", @@ -5139,7 +5139,7 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { "tweetnacl": "^0.14.3" } @@ -5171,7 +5171,7 @@ "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { "inherits": "~2.0.0" } @@ -5338,7 +5338,7 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==" + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" }, "camelcase-css": { "version": "2.0.1", @@ -5349,7 +5349,7 @@ "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -5381,7 +5381,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { "version": "2.4.2", @@ -5474,7 +5474,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", @@ -5521,7 +5521,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collect-v8-coverage": { "version": "1.0.2", @@ -5540,7 +5540,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colord": { "version": "2.9.3", @@ -5642,7 +5642,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concaveman": { "version": "1.2.1", @@ -5670,7 +5670,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-disposition": { "version": "0.5.4", @@ -5698,7 +5698,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-js": { "version": "3.31.1", @@ -5741,7 +5741,7 @@ "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha512-eZ+m1WNhSZutOa/uRblAc9Ut5MQfukFrFMtPSm3bZCA888NmMd5AWXWdgRZ80zd+pTk1P2JrGjg9pUPTvl2PWQ==", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" @@ -5759,7 +5759,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" } } }, @@ -6115,7 +6115,7 @@ "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "requires": { "array-find-index": "^1.0.1" } @@ -6129,7 +6129,7 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { "assert-plus": "^1.0.0" } @@ -6156,7 +6156,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decimal.js": { "version": "10.4.3", @@ -6221,17 +6221,17 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "dequal": { "version": "2.0.3", @@ -6242,7 +6242,7 @@ "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "detect-newline": { "version": "3.1.0", @@ -6457,7 +6457,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -6466,7 +6466,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { "version": "3.1.9", @@ -6502,7 +6502,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "enhanced-resolve": { "version": "5.15.0", @@ -6660,12 +6660,12 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "2.1.0", @@ -7351,7 +7351,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "eventemitter3": { "version": "4.0.7", @@ -7484,7 +7484,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "3.1.3", @@ -7667,7 +7667,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -7717,7 +7717,7 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "fork-ts-checker-webpack-plugin": { "version": "6.5.3", @@ -7908,7 +7908,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-constants": { "version": "1.0.0", @@ -7936,7 +7936,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.2", @@ -7981,7 +7981,7 @@ "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -8009,7 +8009,7 @@ "geojson-equality": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", - "integrity": "sha512-TqG8YbqizP3EfwP5Uw4aLu6pKkg6JQK9uq/XZ1lXQntvTHD1BBKJWhNpJ2M0ax6TuWMP3oyx6Oq7FCIfznrgpQ==", + "integrity": "sha1-oXE3TvBD5dR5eZWEC65GSOB1LXI=", "requires": { "deep-equal": "^1.0.0" } @@ -8045,7 +8045,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==" + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" }, "get-stream": { "version": "6.0.1", @@ -8066,7 +8066,7 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { "assert-plus": "^1.0.0" } @@ -8220,7 +8220,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.5", @@ -8248,7 +8248,7 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { "ansi-regex": "^2.0.0" } @@ -8262,7 +8262,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -8293,7 +8293,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "he": { "version": "1.2.0", @@ -8506,7 +8506,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -8578,7 +8578,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, "immer": { "version": "9.0.21", @@ -8627,7 +8627,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -8683,7 +8683,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-bigint": { "version": "1.0.4", @@ -8761,7 +8761,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" } @@ -8793,7 +8793,7 @@ "is-in-browser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", - "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" }, "is-map": { "version": "2.0.2", @@ -8938,12 +8938,12 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, "is-weakmap": { "version": "2.0.1", @@ -8982,17 +8982,17 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -11531,7 +11531,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { "version": "16.7.0", @@ -11628,7 +11628,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.2.3", @@ -11825,7 +11825,7 @@ "leaflet-fullscreen": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/leaflet-fullscreen/-/leaflet-fullscreen-1.0.2.tgz", - "integrity": "sha512-1Yxm8RZg6KlKX25+hbP2H/wnOAphH7hFcvuADJFb4QZTN7uOSN9Hsci5EZpow8vtNej9OGzu59Jxmn+0qKOO9Q==" + "integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs=" }, "leaflet.locatecontrol": { "version": "0.76.1", @@ -11875,7 +11875,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -11923,7 +11923,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" } } }, @@ -11978,7 +11978,7 @@ "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -12037,7 +12037,7 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" }, "mdn-data": { "version": "2.0.4", @@ -12048,7 +12048,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memfs": { "version": "3.5.3", @@ -12067,7 +12067,7 @@ "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -12084,7 +12084,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge-stream": { "version": "2.0.0", @@ -12101,7 +12101,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "mgrs": { "version": "1.0.0", @@ -12333,7 +12333,7 @@ "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==" + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" } } }, @@ -12375,12 +12375,12 @@ "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -12392,14 +12392,14 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" } } }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "requires": { "abbrev": "1" } @@ -12472,7 +12472,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nwsapi": { "version": "2.2.7", @@ -12488,7 +12488,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-hash": { "version": "3.0.0", @@ -12593,7 +12593,7 @@ "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "requires": { "ee-first": "1.1.1" } @@ -12607,7 +12607,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } @@ -12649,12 +12649,12 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", @@ -12769,7 +12769,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "requires": { "pinkie-promise": "^2.0.0" } @@ -12777,7 +12777,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -12793,7 +12793,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "path-type": { "version": "4.0.0", @@ -12803,7 +12803,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "picocolors": { "version": "1.0.0", @@ -12819,17 +12819,17 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "requires": { "pinkie": "^2.0.0" } @@ -13836,7 +13836,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.9.0", @@ -15300,7 +15300,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -15322,7 +15322,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -15547,7 +15547,7 @@ "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "requires": { "is-finite": "^1.0.0" } @@ -15594,7 +15594,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-from-string": { "version": "2.0.2", @@ -15896,7 +15896,7 @@ "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha512-dYE8LhncfBUar6POCxMTm0Ln+erjeczqEvCJib5/7XNkdw1FkUGgwMPY360FY0FgPWQxHWCx29Jl3oejyGLM9Q==", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "requires": { "js-base64": "^2.1.8", "source-map": "^0.4.2" @@ -15905,7 +15905,7 @@ "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "requires": { "amdefine": ">=0.0.4" } @@ -16053,7 +16053,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setprototypeof": { "version": "1.2.0", @@ -16096,7 +16096,7 @@ "lru-cache": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==" + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" } } }, @@ -16148,7 +16148,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-js": { "version": "1.0.2", @@ -16324,7 +16324,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stdout-stream": { "version": "1.4.1", @@ -16402,7 +16402,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -16487,7 +16487,7 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } @@ -16495,7 +16495,7 @@ "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "requires": { "is-utf8": "^0.2.0" } @@ -16924,7 +16924,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-regex-range": { "version": "5.0.1", @@ -16943,7 +16943,7 @@ "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" }, "tough-cookie": { "version": "2.5.0", @@ -16966,7 +16966,7 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==" + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, "true-case-path": { "version": "1.0.3", @@ -17042,7 +17042,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { "safe-buffer": "^5.0.1" } @@ -17050,7 +17050,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.4.0", @@ -17201,7 +17201,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unquote": { "version": "1.1.1", @@ -17258,7 +17258,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -17281,7 +17281,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "8.3.2", @@ -17316,12 +17316,12 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -18025,7 +18025,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", @@ -18050,7 +18050,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "4.0.2", @@ -18071,7 +18071,7 @@ "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", "dev": true }, "xml-name-validator": { @@ -18134,7 +18134,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", diff --git a/app/src/components/data-grid/autocomplete/AsyncAutocompleteDataGridEditCell.tsx b/app/src/components/data-grid/autocomplete/AsyncAutocompleteDataGridEditCell.tsx new file mode 100644 index 0000000000..68d4a2c653 --- /dev/null +++ b/app/src/components/data-grid/autocomplete/AsyncAutocompleteDataGridEditCell.tsx @@ -0,0 +1,204 @@ +import { mdiMagnify } from '@mdi/js'; +import Icon from '@mdi/react'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import TextField from '@mui/material/TextField'; +import { GridRenderCellParams, GridValidRowModel, useGridApiContext } from '@mui/x-data-grid'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; +import { DebouncedFunc } from 'lodash-es'; +import { useEffect, useState } from 'react'; + +export interface IAsyncAutocompleteDataGridEditCell< + DataGridType extends GridValidRowModel, + ValueType extends string | number +> { + /** + * Data grid props for the cell. + * + * @type {GridRenderCellParams} + * @memberof IAsyncAutocompleteDataGridEditCell + */ + dataGridProps: GridRenderCellParams; + /** + * Function that returns a single option. Used to translate an existing value to its matching option. + * + * @memberof IAsyncAutocompleteDataGridEditCell + */ + getCurrentOption: (value: ValueType) => Promise | null>; + /** + * Search function that returns an array of options to choose from. + * + * @memberof IAsyncAutocompleteDataGridEditCell + */ + getOptions: DebouncedFunc< + ( + searchTerm: string, + onSearchResults: (searchResults: IAutocompleteDataGridOption[]) => void + ) => Promise + >; +} + +/** + * Data grid single value asynchronous autocomplete component for edit. + * + * @template DataGridType + * @template ValueType + * @param {IAsyncAutocompleteDataGridEditCell} props + * @return {*} + */ +const AsyncAutocompleteDataGridEditCell = ( + props: IAsyncAutocompleteDataGridEditCell +) => { + const { dataGridProps, getCurrentOption, getOptions } = props; + + const apiRef = useGridApiContext(); + + // The current data grid value + const dataGridValue = dataGridProps.value; + // The input field value + const [inputValue, setInputValue] = useState['label']>(''); + // The currently selected option + const [currentOption, setCurrentOption] = useState | null>(null); + // The array of options to choose from + const [options, setOptions] = useState[]>([]); + // Is control loading (search in progress) + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + let mounted = true; + + if (!dataGridValue) { + // No current value + return; + } + + if (dataGridValue === currentOption?.value) { + // Existing value matches tracked value + return; + } + + const fetchCurrentOption = async () => { + // Fetch a single option for the current value + const response = await getCurrentOption(dataGridValue); + + if (!mounted) { + return; + } + + if (!response) { + return; + } + + setCurrentOption(response); + }; + + fetchCurrentOption(); + + return () => { + mounted = false; + }; + }, [dataGridValue, currentOption?.value, getCurrentOption]); + + useEffect(() => { + let mounted = true; + + if (inputValue === '') { + // No input value, nothing to search with + setOptions(currentOption ? [currentOption] : []); + return; + } + + // Call async search function + setIsLoading(true); + getOptions(inputValue, (searchResults) => { + if (!mounted) { + return; + } + + setOptions([...searchResults]); + setIsLoading(false); + }); + + return () => { + mounted = false; + }; + }, [inputValue, getOptions, currentOption]); + + function getCurrentValue() { + if (!dataGridValue) { + // No current value + return null; + } + + return currentOption || options.find((option) => dataGridValue === option.value) || null; + } + + return ( + option.label} + isOptionEqualToValue={(option, value) => { + if (!option?.value || !value?.value) { + return false; + } + return option.value === value.value; + }} + filterOptions={createFilterOptions({ limit: 50 })} + onChange={(_, selectedOption) => { + setOptions(selectedOption ? [selectedOption, ...options] : options); + setCurrentOption(selectedOption); + + // Set the data grid cell value with selected options value + apiRef.current.setEditCellValue({ + id: dataGridProps.id, + field: dataGridProps.field, + value: selectedOption?.value + }); + }} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + renderInput={(params) => ( + + + + ), + endAdornment: ( + <> + {isLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + renderOption={(renderProps, renderOption) => { + return ( + + {renderOption.label} + + ); + }} + data-testid={dataGridProps.id} + /> + ); +}; + +export default AsyncAutocompleteDataGridEditCell; diff --git a/app/src/components/data-grid/autocomplete/AutocompleteDataGrid.interface.ts b/app/src/components/data-grid/autocomplete/AutocompleteDataGrid.interface.ts new file mode 100644 index 0000000000..1d8d64fc6d --- /dev/null +++ b/app/src/components/data-grid/autocomplete/AutocompleteDataGrid.interface.ts @@ -0,0 +1,11 @@ +/** + * Defines a single option for a data grid autocomplete control. + * + * @export + * @interface IAutocompleteDataGridOption + * @template ValueType + */ +export interface IAutocompleteDataGridOption { + value: ValueType; + label: string; +} diff --git a/app/src/components/data-grid/autocomplete/AutocompleteDataGridEditCell.tsx b/app/src/components/data-grid/autocomplete/AutocompleteDataGridEditCell.tsx new file mode 100644 index 0000000000..4adc182dca --- /dev/null +++ b/app/src/components/data-grid/autocomplete/AutocompleteDataGridEditCell.tsx @@ -0,0 +1,121 @@ +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import { GridRenderCellParams, GridValidRowModel, useGridApiContext } from '@mui/x-data-grid'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; + +export interface IAutocompleteDataGridEditCellProps< + DataGridType extends GridValidRowModel, + ValueType extends string | number +> { + /** + * Data grid props for the cell. + * + * @type {GridRenderCellParams} + * @memberof IAutocompleteDataGridEditCellProps + */ + dataGridProps: GridRenderCellParams; + /** + * The array of options to choose from. + * + * @type {IAutocompleteDataGridOption[]} + * @memberof IAutocompleteDataGridEditCellProps + */ + options: IAutocompleteDataGridOption[]; + /** + * Function that receives an option, and returns a boolean indicating if that option should be disabled or not. + * + * @memberof IAutocompleteDataGridEditCellProps + */ + getOptionDisabled?: (option: IAutocompleteDataGridOption) => boolean; +} + +/** + * Data grid single value synchronous autocomplete component for edit. + * + * @template DataGridType + * @template ValueType + * @param {IAutocompleteDataGridEditCellProps} props + * @return {*} + */ +const AutocompleteDataGridEditCell = ( + props: IAutocompleteDataGridEditCellProps +) => { + const { dataGridProps, options, getOptionDisabled } = props; + + const apiRef = useGridApiContext(); + + // The current data grid value + const dataGridValue = dataGridProps.value; + + function getCurrentValue() { + if (!dataGridValue) { + // No current value + return null; + } + + const currentOption = options.find((option) => dataGridValue === option.value) ?? null; + + if (!currentOption) { + // No matching options available for current value, set value to null + apiRef.current.setEditCellValue({ + id: dataGridProps.id, + field: dataGridProps.field, + value: null + }); + } + + return currentOption; + } + + return ( + option.label} + isOptionEqualToValue={(option, value) => { + if (!option?.value || !value?.value) { + return false; + } + return option.value === value.value; + }} + filterOptions={createFilterOptions({ limit: 50 })} + getOptionDisabled={getOptionDisabled} + onChange={(_, selectedOption) => { + // Set the data grid cell value with selected options value + apiRef.current.setEditCellValue({ + id: dataGridProps.id, + field: dataGridProps.field, + value: selectedOption?.value + }); + }} + renderInput={(params) => ( + + )} + renderOption={(renderProps, renderOption) => { + return ( + + {renderOption.label} + + ); + }} + data-testid={dataGridProps.id} + /> + ); +}; + +export default AutocompleteDataGridEditCell; diff --git a/app/src/components/data-grid/autocomplete/AutocompleteDataGridViewCell.tsx b/app/src/components/data-grid/autocomplete/AutocompleteDataGridViewCell.tsx new file mode 100644 index 0000000000..01dc2d1954 --- /dev/null +++ b/app/src/components/data-grid/autocomplete/AutocompleteDataGridViewCell.tsx @@ -0,0 +1,39 @@ +import { GridRenderCellParams, GridValidRowModel } from '@mui/x-data-grid'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; + +export interface IAutocompleteDataGridViewCellProps< + DataGridType extends GridValidRowModel, + ValueType extends string | number +> { + /** + * Data grid props for the cell. + * + * @type {GridRenderCellParams} + * @memberof AutocompleteDataGridViewCell + */ + dataGridProps: GridRenderCellParams; + /** + * The array of options to choose from. + * + * @type {IAutocompleteDataGridOption[]} + * @memberof AutocompleteDataGridViewCell + */ + options: IAutocompleteDataGridOption[]; +} + +/** + * Data grid single value synchronous autocomplete component for view. + * + * @template DataGridType + * @template ValueType + * @param {IAutocompleteDataGridViewCellProps} props + * @return {*} + */ +const AutocompleteDataGridViewCell = ( + props: IAutocompleteDataGridViewCellProps +) => { + const { dataGridProps, options } = props; + return <>{options.find((item) => item.value === dataGridProps.value)?.label ?? ''}; +}; + +export default AutocompleteDataGridViewCell; diff --git a/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridEditCell.tsx b/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridEditCell.tsx new file mode 100644 index 0000000000..2ba4ef3762 --- /dev/null +++ b/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridEditCell.tsx @@ -0,0 +1,62 @@ +import { GridRenderCellParams, GridValidRowModel } from '@mui/x-data-grid'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; +import AutocompleteDataGridEditCell from 'components/data-grid/autocomplete/AutocompleteDataGridEditCell'; +import { useMemo } from 'react'; + +export interface IConditionalAutocompleteDataGridEditCellProps< + DataGridType extends GridValidRowModel, + OptionsType extends Record, + ValueType extends string | number +> { + /** + * Data grid props for the cell. + * + * @type {GridRenderCellParams} + * @memberof IConditionalAutocompleteDataGridEditCellProps + */ + dataGridProps: GridRenderCellParams; + /** + * + * + * @type {OptionsType[]} + * @memberof IConditionalAutocompleteDataGridEditCellProps + */ + allOptions: OptionsType[]; + /** + * + * + * @memberof IConditionalAutocompleteDataGridEditCellProps + */ + optionsGetter: (row: DataGridType, allOptions: OptionsType[]) => IAutocompleteDataGridOption[]; +} + +/** + * Data grid single value synchronous autocomplete component for edit. + * + * @template DataGridType + * @template OptionsType + * @template ValueType + * @param {IConditionalAutocompleteDataGridEditCellProps} props + * @return {*} + */ +const ConditionalAutocompleteDataGridEditCell = < + DataGridType extends GridValidRowModel, + OptionsType extends Record, + ValueType extends string | number +>( + props: IConditionalAutocompleteDataGridEditCellProps +) => { + const { dataGridProps, allOptions, optionsGetter } = props; + + const options = useMemo( + function () { + const options = optionsGetter(dataGridProps.row, allOptions); + return options; + }, + [allOptions, dataGridProps.row, optionsGetter] + ); + + return ; +}; + +export default ConditionalAutocompleteDataGridEditCell; diff --git a/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridViewCell.tsx b/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridViewCell.tsx new file mode 100644 index 0000000000..ea5f068032 --- /dev/null +++ b/app/src/components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridViewCell.tsx @@ -0,0 +1,61 @@ +import { GridRenderCellParams, GridValidRowModel } from '@mui/x-data-grid'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; +import { useMemo } from 'react'; + +export interface IConditionalAutocompleteDataGridViewCellProps< + DataGridType extends GridValidRowModel, + OptionsType extends Record, + ValueType extends string | number +> { + /** + * Data grid props for the cell. + * + * @type {GridRenderCellParams} + * @memberof IConditionalAutocompleteDataGridViewCellProps + */ + dataGridProps: GridRenderCellParams; + /** + * + * + * @type {OptionsType[]} + * @memberof IConditionalAutocompleteDataGridViewCellProps + */ + allOptions: OptionsType[]; + /** + * + * + * @memberof IConditionalAutocompleteDataGridViewCellProps + */ + optionsGetter: (row: DataGridType, allOptions: OptionsType[]) => IAutocompleteDataGridOption[]; +} + +/** + * Data grid single value synchronous autocomplete component for view. + * + * @template DataGridType + * @template OptionsType + * @template ValueType + * @param {IConditionalAutocompleteDataGridViewCellProps} props + * @return {*} + */ +const ConditionalAutocompleteDataGridViewCell = < + DataGridType extends GridValidRowModel, + OptionsType extends Record, + ValueType extends string | number +>( + props: IConditionalAutocompleteDataGridViewCellProps +) => { + const { dataGridProps, allOptions, optionsGetter } = props; + + const options = useMemo( + function () { + const options = optionsGetter(dataGridProps.row, allOptions); + return options; + }, + [allOptions, dataGridProps.row, optionsGetter] + ); + + return <>{options.find((item) => item.value === dataGridProps.value)?.label || ''}; +}; + +export default ConditionalAutocompleteDataGridViewCell; diff --git a/app/src/components/data-grid/taxonomy/TaxonomyDataGridEditCell.tsx b/app/src/components/data-grid/taxonomy/TaxonomyDataGridEditCell.tsx new file mode 100644 index 0000000000..0454267711 --- /dev/null +++ b/app/src/components/data-grid/taxonomy/TaxonomyDataGridEditCell.tsx @@ -0,0 +1,67 @@ +import { GridRenderEditCellParams, GridValidRowModel } from '@mui/x-data-grid'; +import AsyncAutocompleteDataGridEditCell from 'components/data-grid/autocomplete/AsyncAutocompleteDataGridEditCell'; +import { IAutocompleteDataGridOption } from 'components/data-grid/autocomplete/AutocompleteDataGrid.interface'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import debounce from 'lodash-es/debounce'; +import { useMemo } from 'react'; + +export interface ITaxonomyDataGridCellProps { + dataGridProps: GridRenderEditCellParams; +} + +/** + * Data grid taxonomy component for edit. + * + * @template DataGridType + * @template ValueType + * @param {ITaxonomyDataGridCellProps} props + * @return {*} + */ +const TaxonomyDataGridEditCell = ( + props: ITaxonomyDataGridCellProps +) => { + const { dataGridProps } = props; + + const biohubApi = useBiohubApi(); + + const getCurrentOption = async ( + speciesId: string | number + ): Promise | null> => { + const response = await biohubApi.taxonomy.getSpeciesFromIds([Number(speciesId)]); + + if (response.searchResponse.length !== 1) { + return null; + } + + return response.searchResponse.map((item) => ({ value: parseInt(item.id) as ValueType, label: item.label }))[0]; + }; + + const getOptions = useMemo( + () => + debounce( + async ( + searchTerm: string, + onSearchResults: (searchedValues: IAutocompleteDataGridOption[]) => void + ) => { + const response = await biohubApi.taxonomy.searchSpecies(searchTerm); + const options = response.searchResponse.map((item) => ({ + value: parseInt(item.id) as ValueType, + label: item.label + })); + onSearchResults(options); + }, + 500 + ), + [biohubApi.taxonomy] + ); + + return ( + + ); +}; + +export default TaxonomyDataGridEditCell; diff --git a/app/src/components/data-grid/taxonomy/TaxonomyDataGridViewCell.tsx b/app/src/components/data-grid/taxonomy/TaxonomyDataGridViewCell.tsx new file mode 100644 index 0000000000..c08f148a2a --- /dev/null +++ b/app/src/components/data-grid/taxonomy/TaxonomyDataGridViewCell.tsx @@ -0,0 +1,38 @@ +import { GridRenderCellParams, GridValidRowModel } from '@mui/x-data-grid'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; + +export interface ITaxonomyDataGridViewCellProps { + dataGridProps: GridRenderCellParams; +} + +/** + * Data grid taxonomy component for view. + * + * @template DataGridType + * @param {ITaxonomyDataGridViewCellProps} props + * @return {*} + */ +const TaxonomyDataGridViewCell = ( + props: ITaxonomyDataGridViewCellProps +) => { + const { dataGridProps } = props; + + const biohubApi = useBiohubApi(); + + const taxonomyDataLoader = useDataLoader(() => biohubApi.taxonomy.getSpeciesFromIds([Number(dataGridProps.value)])); + + taxonomyDataLoader.load(); + + if (!taxonomyDataLoader.isReady) { + return null; + } + + if (taxonomyDataLoader.data?.searchResponse?.length !== 1) { + return null; + } + + return <>{taxonomyDataLoader.data?.searchResponse[0].label}; +}; + +export default TaxonomyDataGridViewCell; diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index cb33042607..27c6dd7d45 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -14,9 +14,9 @@ import { SurveyContext } from './surveyContext'; export interface IObservationRecord { survey_observation_id: number | undefined; wldtaxonomic_units_id: number | undefined; - samplingSite: string | undefined; - samplingMethod: string | undefined; - samplingPeriod: string | undefined; + survey_sample_site_id: number | undefined; + survey_sample_method_id: number | undefined; + survey_sample_period_id: number | undefined; count: number | undefined; observation_date: Date | undefined; observation_time: string | undefined; @@ -61,7 +61,7 @@ export type IObservationsContext = { */ unsavedRecordIds: string[]; /** - * Inidicates whether the observation table has any unsaved changes + * Indicates whether the observation table has any unsaved changes */ hasUnsavedChanges: () => boolean; /** @@ -83,7 +83,7 @@ export type IObservationsContext = { }; export const ObservationsContext = createContext({ - _muiDataGridApiRef: { current: null as unknown as GridApiCommunity }, + _muiDataGridApiRef: null as unknown as React.MutableRefObject, observationsDataLoader: {} as DataLoader, unsavedRecordIds: [], initialRows: [], @@ -143,14 +143,14 @@ export const ObservationsContextProvider = (props: PropsWithChildren { return _getRows().map((row) => { const editRow = _muiDataGridApiRef.current.state.editRows[row.id]; + if (!editRow) { return row; } - return Object.entries(editRow).reduce( (newRow, entry) => ({ ...row, ...newRow, _isModified: true, [entry[0]]: entry[1].value }), {} diff --git a/app/src/contexts/surveyContext.tsx b/app/src/contexts/surveyContext.tsx index 19e460bab1..d3b6c19b9e 100644 --- a/app/src/contexts/surveyContext.tsx +++ b/app/src/contexts/surveyContext.tsx @@ -2,7 +2,11 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { IGetObservationSubmissionResponse } from 'interfaces/useObservationApi.interface'; import { IGetSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; -import { IGetSurveyAttachmentsResponse, IGetSurveyForViewResponse } from 'interfaces/useSurveyApi.interface'; +import { + IGetSampleSiteResponse, + IGetSurveyAttachmentsResponse, + IGetSurveyForViewResponse +} from 'interfaces/useSurveyApi.interface'; import { createContext, PropsWithChildren, useEffect, useMemo } from 'react'; import { useParams } from 'react-router'; @@ -49,6 +53,14 @@ export interface ISurveyContext { */ artifactDataLoader: DataLoader<[project_id: number, survey_id: number], IGetSurveyAttachmentsResponse, unknown>; + /** + * The Data Loader used to load survey sample site data + * + * @type {DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>} + * @memberof ISurveyContext + */ + sampleSiteDataLoader: DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>; + /** * The project ID belonging to the current project * @@ -75,6 +87,7 @@ export const SurveyContext = createContext({ >, summaryDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSummaryResultsResponse, unknown>, artifactDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSurveyAttachmentsResponse, unknown>, + sampleSiteDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>, projectId: -1, surveyId: -1 }); @@ -85,6 +98,8 @@ export const SurveyContextProvider = (props: PropsWithChildren = useParams(); if (!urlParams['id']) { @@ -106,6 +121,7 @@ export const SurveyContextProvider = (props: PropsWithChildren{props.children}; }; diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx index b485455f29..17ace590d8 100644 --- a/app/src/features/funding-sources/components/CreateFundingSource.tsx +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -63,8 +63,9 @@ const CreateFundingSource: React.FC = (props) => { }; const handleSubmitFundingService = async (values: IFundingSourceData) => { - setIsSubmitting(true); try { + setIsSubmitting(true); + await biohubApi.funding.postFundingSource(values); // creation was a success, tell parent to refresh @@ -85,6 +86,7 @@ const CreateFundingSource: React.FC = (props) => { dialogError: (error as APIError).message, dialogErrorDetails: (error as APIError).errors }); + } finally { setIsSubmitting(false); } }; diff --git a/app/src/features/funding-sources/components/EditFundingSource.tsx b/app/src/features/funding-sources/components/EditFundingSource.tsx index 567891cd53..a681529477 100644 --- a/app/src/features/funding-sources/components/EditFundingSource.tsx +++ b/app/src/features/funding-sources/components/EditFundingSource.tsx @@ -70,8 +70,9 @@ const EditFundingSource: React.FC = (props) => { }; const handleSubmitFundingService = async (values: IFundingSourceData) => { - setIsSubmitting(true); try { + setIsSubmitting(true); + await biohubApi.funding.putFundingSource(values); // creation was a success, tell parent to refresh @@ -92,6 +93,7 @@ const EditFundingSource: React.FC = (props) => { dialogError: (error as APIError).message, dialogErrorDetails: (error as APIError).errors }); + } finally { setIsSubmitting(false); } }; diff --git a/app/src/features/surveys/SurveyRouter.tsx b/app/src/features/surveys/SurveyRouter.tsx index 1b2a3e2d44..7268b22d41 100644 --- a/app/src/features/surveys/SurveyRouter.tsx +++ b/app/src/features/surveys/SurveyRouter.tsx @@ -6,6 +6,7 @@ import { Redirect, Switch } from 'react-router'; import RouteWithTitle from 'utils/RouteWithTitle'; import { getTitle } from 'utils/Utils'; import EditSurveyPage from './edit/EditSurveyPage'; +import SamplingSiteEditPage from './observations/sampling-sites/edit/SamplingSiteEditPage'; import SamplingSitePage from './observations/sampling-sites/SamplingSitePage'; import { SurveyObservationPage } from './observations/SurveyObservationPage'; @@ -35,6 +36,13 @@ const SurveyRouter: React.FC = () => { + + + + { + const sampleSites: ISampleSiteSelectProps[] = []; + const sampleMethods: ISampleMethodSelectProps[] = []; + const samplePeriods: ISamplePeriodSelectProps[] = []; const observationsContext = useContext(ObservationsContext); + const surveyContext = useContext(SurveyContext); + const codesContext = useContext(CodesContext); + const [isSaving, setIsSaving] = useState(false); const [showConfirmRemoveAllDialog, setShowConfirmRemoveAllDialog] = useState(false); - const handleSaveChanges = () => { + const handleSaveChanges = async () => { setIsSaving(true); - observationsContext.saveRecords().finally(() => { + return observationsContext.saveRecords().finally(() => { setIsSaving(false); }); }; const showSaveButton = observationsContext.hasUnsavedChanges(); + if (surveyContext.sampleSiteDataLoader.data && codesContext.codesDataLoader.data) { + // loop through and collect all sites + surveyContext.sampleSiteDataLoader.data.sampleSites.forEach((site) => { + sampleSites.push({ + survey_sample_site_id: site.survey_sample_site_id, + sample_site_name: site.name + }); + + // loop through and collect all methods for all sites + site.sample_methods?.forEach((method) => { + sampleMethods.push({ + survey_sample_method_id: method.survey_sample_method_id, + survey_sample_site_id: site.survey_sample_site_id, + sample_method_name: + getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method.method_lookup_id) ?? '' + }); + + // loop through and collect all periods for all methods for all sites + method.sample_periods?.forEach((period) => { + samplePeriods.push({ + survey_sample_period_id: period.survey_sample_period_id, + survey_sample_method_id: period.survey_sample_method_id, + sample_period_name: `${period.start_date} - ${period.end_date}` + }); + }); + }); + }); + } + return ( <> { - + diff --git a/app/src/features/surveys/observations/ObservationsTable.tsx b/app/src/features/surveys/observations/ObservationsTable.tsx index 60add2acc7..8509df6ca7 100644 --- a/app/src/features/surveys/observations/ObservationsTable.tsx +++ b/app/src/features/surveys/observations/ObservationsTable.tsx @@ -2,12 +2,60 @@ import { mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import IconButton from '@mui/material/IconButton'; import { DataGrid, GridColDef, GridEditInputCell, GridEventListener, GridRowModelUpdate } from '@mui/x-data-grid'; +import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import AutocompleteDataGridEditCell from 'components/data-grid/autocomplete/AutocompleteDataGridEditCell'; +import AutocompleteDataGridViewCell from 'components/data-grid/autocomplete/AutocompleteDataGridViewCell'; +import ConditionalAutocompleteDataGridEditCell from 'components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridEditCell'; +import ConditionalAutocompleteDataGridViewCell from 'components/data-grid/conditional-autocomplete/ConditionalAutocompleteDataGridViewCell'; +import TaxonomyDataGridEditCell from 'components/data-grid/taxonomy/TaxonomyDataGridEditCell'; +import TaxonomyDataGridViewCell from 'components/data-grid/taxonomy/TaxonomyDataGridViewCell'; import YesNoDialog from 'components/dialog/YesNoDialog'; import { ObservationsTableI18N } from 'constants/i18n'; import { IObservationTableRow, ObservationsContext } from 'contexts/observationsContext'; +import moment from 'moment'; import { useContext, useEffect, useState } from 'react'; -const ObservationsTable = () => { +export interface ISampleSiteSelectProps { + survey_sample_site_id: number; + sample_site_name: string; +} + +export interface ISampleMethodSelectProps { + survey_sample_method_id: number; + survey_sample_site_id: number; + sample_method_name: string; +} + +export interface ISamplePeriodSelectProps { + survey_sample_period_id: number; + survey_sample_method_id: number; + sample_period_name: string; +} +export interface ISpeciesObservationTableProps { + sample_sites: { + survey_sample_site_id: number; + sample_site_name: string; + }[]; + sample_methods: { + survey_sample_method_id: number; + survey_sample_site_id: number; + sample_method_name: string; + }[]; + sample_periods: { + survey_sample_period_id: number; + survey_sample_method_id: number; + sample_period_name: string; + }[]; +} + +const ObservationsTable = (props: ISpeciesObservationTableProps) => { + const { sample_sites, sample_methods, sample_periods } = props; + const observationsContext = useContext(ObservationsContext); + const { observationsDataLoader } = observationsContext; + + const apiRef = observationsContext._muiDataGridApiRef; + const observationColumns: GridColDef[] = [ { field: 'wldtaxonomic_units_id', @@ -16,39 +64,118 @@ const ObservationsTable = () => { flex: 1, minWidth: 250, disableColumnMenu: true, - - // TODO: To be addressed by https://apps.nrs.gov.bc.ca/int/jira/browse/SIMSBIOHUB-288 - renderCell: () => 'Moose (Alces Americanus)' + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + return ; + }, + renderEditCell: (params) => { + return ; + } }, { - field: 'samplingSite', + field: 'survey_sample_site_id', headerName: 'Sampling Site', editable: true, - type: 'singleSelect', - valueOptions: ['Site 1', 'Site 2', 'Site 3', 'Site 4'], flex: 1, minWidth: 200, - disableColumnMenu: true + disableColumnMenu: true, + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + return ( + ({ + label: item.sample_site_name, + value: item.survey_sample_site_id + }))} + /> + ); + }, + renderEditCell: (params) => { + return ( + ({ + label: item.sample_site_name, + value: item.survey_sample_site_id + }))} + /> + ); + } }, { - field: 'samplingMethod', + field: 'survey_sample_method_id', headerName: 'Sampling Method', editable: true, - type: 'singleSelect', - valueOptions: ['Method 1', 'Method 2', 'Method 3', 'Method 4'], flex: 1, minWidth: 200, - disableColumnMenu: true + disableColumnMenu: true, + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + return ( + { + return allOptions + .filter((item) => item.survey_sample_site_id === row.survey_sample_site_id) + .map((item) => ({ label: item.sample_method_name, value: item.survey_sample_method_id })); + }} + allOptions={sample_methods} + /> + ); + }, + renderEditCell: (params) => { + return ( + { + return allOptions + .filter((item) => item.survey_sample_site_id === row.survey_sample_site_id) + .map((item) => ({ label: item.sample_method_name, value: item.survey_sample_method_id })); + }} + allOptions={sample_methods} + /> + ); + } }, { - field: 'samplingPeriod', + field: 'survey_sample_period_id', headerName: 'Sampling Period', editable: true, - type: 'singleSelect', - valueOptions: ['Period 1', 'Period 2', 'Period 3', 'Period 4', 'Undefined'], flex: 1, minWidth: 200, - disableColumnMenu: true + disableColumnMenu: true, + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + return ( + { + return allOptions + .filter((item) => item.survey_sample_method_id === row.survey_sample_method_id) + .map((item) => ({ label: item.sample_period_name, value: item.survey_sample_period_id })); + }} + allOptions={sample_periods} + /> + ); + }, + renderEditCell: (params) => { + return ( + { + return allOptions + .filter((item) => item.survey_sample_method_id === row.survey_sample_method_id) + .map((item) => ({ label: item.sample_period_name, value: item.survey_sample_period_id })); + }} + allOptions={sample_periods} + /> + ); + } }, { field: 'count', @@ -57,6 +184,8 @@ const ObservationsTable = () => { type: 'number', minWidth: 100, disableColumnMenu: true, + headerAlign: 'left', + align: 'left', renderEditCell: (params) => ( { editable: true, type: 'date', minWidth: 150, - valueGetter: (params) => (params.row.observation_date ? new Date(params.row.observation_date) : null), - disableColumnMenu: true + valueGetter: (params) => (params.row.observation_date ? moment(params.row.observation_date).toDate() : null), + disableColumnMenu: true, + headerAlign: 'left', + align: 'left' }, { field: 'observation_time', headerName: 'Time', editable: true, - type: 'time', + type: 'string', width: 150, - disableColumnMenu: true + disableColumnMenu: true, + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + if (!params.value) { + return null; + } + + if (moment.isMoment(params.value)) { + return <>{params.value.format('HH:mm')}; + } + + return <>{moment(params.value, 'HH:mm:ss').format('HH:mm')}; + }, + renderEditCell: (params) => { + return ( + + { + apiRef?.current.setEditCellValue({ id: params.id, field: params.field, value: value }); + }} + onAccept={(value) => { + apiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value: value?.format('HH:mm:ss') + }); + }} + ampm={false} + /> + + ); + } }, { field: 'latitude', @@ -91,6 +255,8 @@ const ObservationsTable = () => { editable: true, width: 150, disableColumnMenu: true, + headerAlign: 'left', + align: 'left', renderCell: (params) => String(params.row.latitude) }, { @@ -100,6 +266,8 @@ const ObservationsTable = () => { editable: true, width: 150, disableColumnMenu: true, + headerAlign: 'left', + align: 'left', renderCell: (params) => String(params.row.longitude) }, { @@ -122,10 +290,6 @@ const ObservationsTable = () => { } ]; - const observationsContext = useContext(ObservationsContext); - const { observationsDataLoader } = observationsContext; - const apiRef = observationsContext._muiDataGridApiRef; - const [deletingObservation, setDeletingObservation] = useState(null); const showConfirmDeleteDialog = Boolean(deletingObservation); @@ -140,7 +304,6 @@ const ObservationsTable = () => { observationsContext.setInitialRows(rows); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [observationsDataLoader.data]); @@ -154,7 +317,7 @@ const ObservationsTable = () => { const handleDeleteRow = (id: string | number) => { observationsContext.markRecordWithUnsavedChanges(id); - apiRef.current.updateRows([{ id, _action: 'delete' } as GridRowModelUpdate]); + apiRef?.current.updateRows([{ id, _action: 'delete' } as GridRowModelUpdate]); }; const handleRowEditStop: GridEventListener<'rowEditStop'> = (_params, event) => { @@ -164,11 +327,11 @@ const ObservationsTable = () => { const handleCellClick: GridEventListener<'cellClick'> = (params, event) => { const { id } = params.row; - if (apiRef.current.state.editRows[id]) { + if (apiRef?.current.state.editRows[id]) { return; } - apiRef.current.startRowEditMode({ id, fieldToFocus: params.field }); + apiRef?.current.startRowEditMode({ id, fieldToFocus: params.field }); observationsContext.markRecordWithUnsavedChanges(id); }; @@ -229,15 +392,6 @@ const ObservationsTable = () => { '& .MuiDataGrid-columnHeaders': { position: 'relative' }, - '& .MuiDataGrid-columnHeaders:after': { - content: "''", - position: 'absolute', - right: 0, - width: '96px', - height: '80px', - borderLeft: '1px solid #ccc', - background: '#fff' - }, '& .MuiDataGrid-actionsCell': { gap: 0 } diff --git a/app/src/features/surveys/observations/SurveyObservationHeader.tsx b/app/src/features/surveys/observations/SurveyObservationHeader.tsx index 000b9e5da6..25df800057 100644 --- a/app/src/features/surveys/observations/SurveyObservationHeader.tsx +++ b/app/src/features/surveys/observations/SurveyObservationHeader.tsx @@ -1,7 +1,7 @@ import Breadcrumbs from '@mui/material/Breadcrumbs'; -import Link from '@mui/material/Link'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; +import { Link } from 'react-router-dom'; export interface SurveyObservationHeaderProps { project_id: number; @@ -26,8 +26,10 @@ const SurveyObservationHeader: React.FC = (props) sx={{ mb: 1 }}> - - {survey_name} + + + {survey_name} + Manage Survey Observations diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSiteHeader.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSiteHeader.tsx index fea79f5f66..177987d151 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSiteHeader.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSiteHeader.tsx @@ -1,9 +1,11 @@ import { LoadingButton } from '@mui/lab'; -import { Breadcrumbs, Button, Link, Paper, Theme, Typography } from '@mui/material'; +import { Breadcrumbs, Button, Paper, Theme, Typography } from '@mui/material'; import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; import { makeStyles } from '@mui/styles'; import { useFormikContext } from 'formik'; import { useHistory } from 'react-router'; +import { Link as RouterLink } from 'react-router-dom'; import { ICreateSamplingSiteRequest } from './SamplingSitePage'; const useStyles = makeStyles((theme: Theme) => ({ @@ -20,13 +22,15 @@ export interface ISamplingSiteHeaderProps { survey_id: number; survey_name: string; is_submitting: boolean; + title: string; + breadcrumb: string; } export const SamplingSiteHeader: React.FC = (props) => { const classes = useStyles(); const history = useHistory(); const formikProps = useFormikContext(); - const { project_id, survey_id, survey_name, is_submitting } = props; + const { project_id, survey_id, survey_name, is_submitting, title, breadcrumb } = props; return ( <> = (props) => px: 3 }}> - - {survey_name} + + + {survey_name} + - Manage Survey Observations + component={RouterLink} + to={`/admin/projects/${project_id}/surveys/${survey_id}/observations`} + underline="none"> + + Manage Survey Observations + - Add Sampling Sites + {breadcrumb} @@ -58,7 +69,7 @@ export const SamplingSiteHeader: React.FC = (props) => sx={{ ml: '-2px' }}> - New Sampling Site + {title} { - const history = useHistory(); + const surveyContext = useContext(SurveyContext); + const codesContext = useContext(CodesContext); + + useEffect(() => { + codesContext.codesDataLoader.load(); + }, [codesContext.codesDataLoader]); + + surveyContext.sampleSiteDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedSampleSiteId, setSelectedSampleSiteId] = useState(); + + const handleMenuClick = (event: React.MouseEvent, sample_site_id: number) => { + setAnchorEl(event.currentTarget); + setSelectedSampleSiteId(sample_site_id); + }; + + if ( + !surveyContext.sampleSiteDataLoader.data || + (surveyContext.sampleSiteDataLoader.isLoading && !codesContext.codesDataLoader.data) || + codesContext.codesDataLoader.isLoading + ) { + // TODO Fix styling: spinner loads in the corner of the component + return ; + } return ( - - + setAnchorEl(null)} + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right' }}> - + + + + Edit Details + + console.log('DELETE THIS SAMPLING SITE')}> + + + + Remove + + + + - Sampling Sites - - - - + + Sampling Sites + + + - - No Sampling Sites - - - - - - - Sampling Site 1 - - - - - - - - - - - Method 1 - - - - - - - YYYY-MM-DD to YYYY-MM-DD - - - - - YYYY-MM-DD to YYYY-MM-DD - - - - - + {!surveyContext.sampleSiteDataLoader.data.sampleSites.length && ( + + No Sampling Sites + + )} - - - - - Sampling Site 1 - - - - - - - - - { + return ( + - - Method 1 - - - - - - - YYYY-MM-DD to YYYY-MM-DD - - - - - YYYY-MM-DD to YYYY-MM-DD - - - - - + + + + {sampleSite.name} + + + ) => + handleMenuClick(event, sampleSite.survey_sample_site_id) + } + aria-label="settings"> + + + + + + {sampleSite.sample_methods?.map((sampleMethod) => { + return ( + + + + {getCodesName( + codesContext.codesDataLoader.data, + 'sample_methods', + sampleMethod.method_lookup_id + )} + + + {sampleMethod.sample_periods?.map((samplePeriod) => { + return ( + + + + {samplePeriod.start_date} to {samplePeriod.end_date} + + + + ); + })} + + + + ); + })} + + + + ); + })} + - + ); }; diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSitePage.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSitePage.tsx index 662186abe2..8262eea049 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSitePage.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSitePage.tsx @@ -89,12 +89,17 @@ const SamplingSitePage = () => { }; const handleSubmit = async (values: ICreateSamplingSiteRequest) => { - setIsSubmitting(true); try { + setIsSubmitting(true); + await biohubApi.samplingSite.createSamplingSites(surveyContext.projectId, surveyContext.surveyId, values); // Disable cancel prompt so we can navigate away from the page after saving setEnableCancelCheck(false); + + // Refresh the context, so the next page loads with the latest data + surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + // create complete, navigate back to observations page history.push(`/admin/projects/${surveyContext.projectId}/surveys/${surveyContext.surveyId}/observations`); } catch (error) { @@ -104,7 +109,6 @@ const SamplingSitePage = () => { dialogError: (error as APIError).message, dialogErrorDetails: (error as APIError)?.errors }); - } finally { setIsSubmitting(false); } }; @@ -173,6 +177,8 @@ const SamplingSitePage = () => { survey_id={surveyContext.surveyId} survey_name={surveyContext.surveyDataLoader.data.surveyData.survey_details.survey_name} is_submitting={isSubmitting} + title="New Sampling Site" + breadcrumb="Add Sampling Sites" /> diff --git a/app/src/features/surveys/observations/sampling-sites/components/SampleSiteFileUploadItemSubtext.tsx b/app/src/features/surveys/observations/sampling-sites/components/SampleSiteFileUploadItemSubtext.tsx index d7f1feb234..842349ac5b 100644 --- a/app/src/features/surveys/observations/sampling-sites/components/SampleSiteFileUploadItemSubtext.tsx +++ b/app/src/features/surveys/observations/sampling-sites/components/SampleSiteFileUploadItemSubtext.tsx @@ -18,7 +18,7 @@ const SampleSiteFileUploadItemSubtext = (props: ISubtextProps) => { } return ( - + {subtext} ); diff --git a/app/src/features/surveys/observations/sampling-sites/edit/SamplingSiteEditPage.tsx b/app/src/features/surveys/observations/sampling-sites/edit/SamplingSiteEditPage.tsx new file mode 100644 index 0000000000..3c4031851b --- /dev/null +++ b/app/src/features/surveys/observations/sampling-sites/edit/SamplingSiteEditPage.tsx @@ -0,0 +1,144 @@ +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import { grey } from '@mui/material/colors'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { CreateSamplingSiteI18N } from 'constants/i18n'; +import { DialogContext } from 'contexts/dialogContext'; +import { SurveyContext } from 'contexts/surveyContext'; +import { FormikProps } from 'formik'; +import History from 'history'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useContext, useRef, useState } from 'react'; +import { Prompt, useHistory, useParams } from 'react-router'; +import SamplingSiteHeader from '../SamplingSiteHeader'; +import SampleSiteEditForm, { IEditSamplingSiteRequest } from './components/SampleSiteEditForm'; + +const SamplingSiteEditPage = () => { + const history = useHistory(); + const biohubApi = useBiohubApi(); + const urlParams: Record = useParams(); + const surveySampleSiteId = Number(urlParams['survey_sample_site_id']); + + const surveyContext = useContext(SurveyContext); + const dialogContext = useContext(DialogContext); + + const formikRef = useRef>(null); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [enableCancelCheck, setEnableCancelCheck] = useState(true); + + const showCreateErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ + dialogTitle: CreateSamplingSiteI18N.createErrorTitle, + dialogText: CreateSamplingSiteI18N.createErrorText, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + }, + ...textDialogProps, + open: true + }); + }; + + const handleSubmit = async (values: IEditSamplingSiteRequest) => { + try { + setIsSubmitting(true); + + await biohubApi.samplingSite.editSampleSite( + surveyContext.projectId, + surveyContext.surveyId, + surveySampleSiteId, + values + ); + + // Disable cancel prompt so we can navigate away from the page after saving + setEnableCancelCheck(false); + + // Refresh the context, so the next page loads with the latest data + surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + + // create complete, navigate back to observations page + history.push(`/admin/projects/${surveyContext.projectId}/surveys/${surveyContext.surveyId}/observations`); + } catch (error) { + showCreateErrorDialog({ + dialogTitle: CreateSamplingSiteI18N.createErrorTitle, + dialogText: CreateSamplingSiteI18N.createErrorText, + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError)?.errors + }); + setIsSubmitting(false); + } + }; + + /** + * Intercepts all navigation attempts (when used with a `Prompt`). + * + * Returning true allows the navigation, returning false prevents it. + * + * @param {History.Location} location + * @return {*} + */ + const handleLocationChange = (location: History.Location, action: History.Action) => { + if (!dialogContext.yesNoDialogProps.open) { + // If the cancel dialog is not open: open it + dialogContext.setYesNoDialog({ + open: true, + dialogTitle: CreateSamplingSiteI18N.cancelTitle, + dialogText: CreateSamplingSiteI18N.cancelText, + onClose: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onNo: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onYes: () => { + dialogContext.setYesNoDialog({ open: false }); + history.push(location.pathname); + } + }); + return false; + } + + // If the cancel dialog is already open and another location change action is triggered: allow it + return true; + }; + + if (!surveyContext.surveyDataLoader.data || !surveyContext.sampleSiteDataLoader.data) { + return ; + } + + return ( + <> + + + + + + + + + + + + ); +}; + +export default SamplingSiteEditPage; diff --git a/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteEditForm.tsx b/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteEditForm.tsx new file mode 100644 index 0000000000..a5458391c7 --- /dev/null +++ b/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteEditForm.tsx @@ -0,0 +1,136 @@ +import { LoadingButton } from '@mui/lab'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Paper from '@mui/material/Paper'; +import { Theme } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; +import { Container } from '@mui/system'; +import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent'; +import { SurveyContext } from 'contexts/surveyContext'; +import { ISurveySampleMethodData, SamplingSiteMethodYupSchema } from 'features/surveys/components/MethodForm'; +import SamplingMethodForm from 'features/surveys/components/SamplingMethodForm'; +import SurveySamplingSiteImportForm from 'features/surveys/components/SurveySamplingSiteImportForm'; +import { Formik, FormikProps } from 'formik'; +import { Feature } from 'geojson'; +import { useContext } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import yup from 'utils/YupSchema'; +import SampleSiteGeneralInformationForm from './SampleSiteGeneralInformationForm'; + +const useStyles = makeStyles((theme: Theme) => ({ + actionButton: { + minWidth: '6rem', + '& + button': { + marginLeft: '0.5rem' + } + }, + sectionDivider: { + height: '1px', + marginTop: theme.spacing(5), + marginBottom: theme.spacing(5) + } +})); + +export interface IEditSamplingSiteRequest { + name: string; + description: string; + survey_id: number; + survey_sample_sites: Feature[]; // extracted list from shape files + methods: ISurveySampleMethodData[]; +} + +export interface ISampleSiteEditForm { + handleSubmit: (formikData: IEditSamplingSiteRequest) => void; + formikRef: React.RefObject>; + isSubmitting: boolean; +} + +export const samplingSiteYupSchema = yup.object({ + name: yup.string().default(''), + description: yup.string().default(''), + survey_sample_sites: yup.array(yup.object()).min(1, 'At least one sampling site location is required'), + methods: yup + .array(yup.object().concat(SamplingSiteMethodYupSchema)) + .min(1, 'At least one sampling method is required') +}); + +const SampleSiteEditForm: React.FC = (props) => { + const classes = useStyles(); + + const surveyContext = useContext(SurveyContext); + + return ( + <> + + + + + }> + + + + }> + + + + }> + + + + + { + props.formikRef.current?.submitForm(); + }} + className={classes.actionButton}> + Save and Exit + + + + + + + + + ); +}; + +export default SampleSiteEditForm; diff --git a/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteGeneralInformationForm.tsx b/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteGeneralInformationForm.tsx new file mode 100644 index 0000000000..2e79fada95 --- /dev/null +++ b/app/src/features/surveys/observations/sampling-sites/edit/components/SampleSiteGeneralInformationForm.tsx @@ -0,0 +1,35 @@ +import Grid from '@mui/material/Grid'; +import CustomTextField from 'components/fields/CustomTextField'; +import React from 'react'; + +/** + * Create survey - general information fields + * + * @return {*} + */ +const SampleSiteGeneralInformationForm: React.FC = (props) => { + return ( + <> + + + + + + + + + + ); +}; + +export default SampleSiteGeneralInformationForm; diff --git a/app/src/features/surveys/view/SurveyDetails.test.tsx b/app/src/features/surveys/view/SurveyDetails.test.tsx index 6e1dc2a904..314ba8380a 100644 --- a/app/src/features/surveys/view/SurveyDetails.test.tsx +++ b/app/src/features/surveys/view/SurveyDetails.test.tsx @@ -29,6 +29,7 @@ describe('SurveyDetails', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; it('renders correctly', async () => { const { getByText } = render( @@ -39,7 +40,8 @@ describe('SurveyDetails', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> diff --git a/app/src/features/surveys/view/SurveyHeader.test.tsx b/app/src/features/surveys/view/SurveyHeader.test.tsx index 325a3f29de..23e8a9f2cc 100644 --- a/app/src/features/surveys/view/SurveyHeader.test.tsx +++ b/app/src/features/surveys/view/SurveyHeader.test.tsx @@ -37,6 +37,9 @@ const mockSurveyContext: ISurveyContext = { observationDataLoader: { data: null } as DataLoader, + sampleSiteDataLoader: { + data: null + } as DataLoader, surveyId: 1, projectId: 1 }; diff --git a/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx b/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx index 19f3ed898f..a430ebca63 100644 --- a/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx +++ b/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx @@ -29,6 +29,7 @@ describe('SurveyGeneralInformation', () => { any >; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId } = render( @@ -39,7 +40,8 @@ describe('SurveyGeneralInformation', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -69,6 +71,7 @@ describe('SurveyGeneralInformation', () => { any >; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId } = render( @@ -79,7 +82,8 @@ describe('SurveyGeneralInformation', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -98,6 +102,7 @@ describe('SurveyGeneralInformation', () => { any >; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { container } = render( @@ -108,7 +113,8 @@ describe('SurveyGeneralInformation', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> diff --git a/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx b/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx index 0331cf0bb4..7349c22468 100644 --- a/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx +++ b/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx @@ -20,6 +20,7 @@ describe('SurveyProprietaryData', () => { >; const mockSummaryDataLoader = { data: null } as DataLoader; const mockArtifactDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -51,6 +53,7 @@ describe('SurveyProprietaryData', () => { >; const mockSummaryDataLoader = { data: null } as DataLoader; const mockArtifactDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -80,6 +84,7 @@ describe('SurveyProprietaryData', () => { >; const mockSummaryDataLoader = { data: null } as DataLoader; const mockArtifactDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { container } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> diff --git a/app/src/features/surveys/view/components/SurveyPurposeAndMethodologyData.test.tsx b/app/src/features/surveys/view/components/SurveyPurposeAndMethodologyData.test.tsx index 47df56400e..228cfb5505 100644 --- a/app/src/features/surveys/view/components/SurveyPurposeAndMethodologyData.test.tsx +++ b/app/src/features/surveys/view/components/SurveyPurposeAndMethodologyData.test.tsx @@ -23,6 +23,7 @@ describe('SurveyPurposeAndMethodologyData', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId, getAllByTestId } = render( @@ -33,7 +34,8 @@ describe('SurveyPurposeAndMethodologyData', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -72,6 +74,7 @@ describe('SurveyPurposeAndMethodologyData', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { getByTestId, getAllByTestId } = render( @@ -82,7 +85,8 @@ describe('SurveyPurposeAndMethodologyData', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> diff --git a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx index f9acc05530..d6147e617c 100644 --- a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx +++ b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx @@ -44,6 +44,7 @@ describe('SurveyStudyArea', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { container } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -78,6 +80,7 @@ describe('SurveyStudyArea', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { container, queryByTestId } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -104,6 +108,7 @@ describe('SurveyStudyArea', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const { container, getByTestId } = render( { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -134,6 +140,7 @@ describe('SurveyStudyArea', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; const mockProjectAuthStateContext: IProjectAuthStateContext = { getProjectParticipant: () => null, @@ -153,7 +160,8 @@ describe('SurveyStudyArea', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> @@ -224,6 +232,7 @@ describe('SurveyStudyArea', () => { const mockArtifactDataLoader = { data: null } as DataLoader; const mockObservationsDataLoader = { data: null } as DataLoader; const mockSummaryDataLoader = { data: null } as DataLoader; + const mockSampleSiteDataLoader = { data: null } as DataLoader; mockUseApi.survey.getSurveyForView.mockResolvedValue({ surveyData: { @@ -260,7 +269,8 @@ describe('SurveyStudyArea', () => { surveyDataLoader: mockSurveyDataLoader, artifactDataLoader: mockArtifactDataLoader, observationDataLoader: mockObservationsDataLoader, - summaryDataLoader: mockSummaryDataLoader + summaryDataLoader: mockSummaryDataLoader, + sampleSiteDataLoader: mockSampleSiteDataLoader }}> diff --git a/app/src/features/surveys/view/survey-observations/SurveyObservations.test.tsx b/app/src/features/surveys/view/survey-observations/SurveyObservations.test.tsx index 7545539804..665aeb482d 100644 --- a/app/src/features/surveys/view/survey-observations/SurveyObservations.test.tsx +++ b/app/src/features/surveys/view/survey-observations/SurveyObservations.test.tsx @@ -38,6 +38,7 @@ describe('SurveyObservations', () => { artifactDataLoader: {} as unknown as DataLoader, surveyDataLoader: {} as unknown as DataLoader, summaryDataLoader: {} as unknown as DataLoader, + sampleSiteDataLoader: {} as unknown as DataLoader, surveyId: 1, projectId: 1 }; diff --git a/app/src/hooks/api/useSamplingSiteApi.ts b/app/src/hooks/api/useSamplingSiteApi.ts index 7177591682..05c57b069f 100644 --- a/app/src/hooks/api/useSamplingSiteApi.ts +++ b/app/src/hooks/api/useSamplingSiteApi.ts @@ -1,5 +1,6 @@ import { AxiosInstance } from 'axios'; import { ICreateSamplingSiteRequest } from 'features/surveys/observations/sampling-sites/SamplingSitePage'; +import { IGetSampleSiteResponse } from 'interfaces/useSurveyApi.interface'; /** * Returns a set of supported api methods for working with search functionality @@ -9,9 +10,12 @@ import { ICreateSamplingSiteRequest } from 'features/surveys/observations/sampli */ const useSamplingSiteApi = (axios: AxiosInstance) => { /** - * Get search results (spatial) + * Create Sampling Sites * - * @return {*} {Promise} + * @param {number} projectId + * @param {number} surveyId + * @param {ICreateSamplingSiteRequest} samplingSite + * @return {*} {Promise} */ const createSamplingSites = async ( projectId: number, @@ -21,8 +25,40 @@ const useSamplingSiteApi = (axios: AxiosInstance) => { await axios.post(`/api/project/${projectId}/survey/${surveyId}/sample-site`, samplingSite); }; + /** + * Get Sample Sites + * + * @param {number} projectId + * @param {number} surveyId + * @return {*} {Promise} + */ + const getSampleSites = async (projectId: number, surveyId: number): Promise => { + const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/sample-site`); + return data; + }; + + /** + * Edit Sample Site + * + * @param {number} projectId + * @param {number} surveyId + * @param {number} sampleSiteId + * @param {ICreateSamplingSiteRequest} sampleSite + * @return {*} {Promise} + */ + const editSampleSite = async ( + projectId: number, + surveyId: number, + sampleSiteId: number, + sampleSite: ICreateSamplingSiteRequest + ): Promise => { + await axios.put(`/api/project/${projectId}/survey/${surveyId}/sample-site/${sampleSiteId}`, sampleSite); + }; + return { - createSamplingSites + createSamplingSites, + getSampleSites, + editSampleSite }; }; diff --git a/app/src/interfaces/useSurveyApi.interface.ts b/app/src/interfaces/useSurveyApi.interface.ts index f8bc44671c..01026dcdce 100644 --- a/app/src/interfaces/useSurveyApi.interface.ts +++ b/app/src/interfaces/useSurveyApi.interface.ts @@ -343,3 +343,46 @@ export type IEditSurveyRequest = IGeneralInformationForm & IProprietaryDataForm & IUpdateAgreementsForm & { partnerships: IGetSurveyForViewResponsePartnerships } & ISurveySiteSelectionForm & IParticipantsJobForm; + +export interface IGetSampleSiteResponse { + sampleSites: IGetSampleLocationRecord[]; +} +export interface IGetSampleLocationRecord { + survey_sample_site_id: number; + survey_id: number; + name: string; + description: string; + geojson: Feature[]; + geography: string; + create_date: string; + create_user: number; + update_date: string | null; + update_user: number | null; + revision_count: number; + sample_methods: IGetSampleMethodRecord[] | undefined; +} + +export interface IGetSampleMethodRecord { + survey_sample_method_id: number; + survey_sample_site_id: number; + method_lookup_id: number; + description: string; + create_date: string; + create_user: number; + update_date: string | null; + update_user: number | null; + revision_count: number; + sample_periods: IGetSamplePeriodRecord[] | undefined; +} + +export interface IGetSamplePeriodRecord { + survey_sample_period_id: number; + survey_sample_method_id: number; + start_date: string; + end_date: string; + create_date: string; + create_user: number; + update_date: string | null; + update_user: number | null; + revision_count: number; +} diff --git a/database/src/migrations/20230929134200_update_survey_observations.ts b/database/src/migrations/20230929134200_update_survey_observations.ts new file mode 100644 index 0000000000..73fe96f10c --- /dev/null +++ b/database/src/migrations/20230929134200_update_survey_observations.ts @@ -0,0 +1,192 @@ +import { Knex } from 'knex'; + +/** + * Create survey observation table. + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + + ---------------------------------------------------------------------------------------- + -- Add Sampling Site Data to Survey Observation table + ---------------------------------------------------------------------------------------- + + SET search_path=biohub; + + ALTER TABLE survey_observation ADD survey_sample_site_id integer NOT NULL; + ALTER TABLE survey_observation ADD survey_sample_method_id integer NOT NULL; + ALTER TABLE survey_observation ADD survey_sample_period_id integer NOT NULL; + + COMMENT ON COLUMN survey_observation.survey_sample_site_id IS 'A foreign key pointing to the survey sample site table.'; + COMMENT ON COLUMN survey_observation.survey_sample_method_id IS 'A foreign key pointing to the survey sample method table.'; + COMMENT ON COLUMN survey_observation.survey_sample_period_id IS 'A foreign key pointing to the survey sample period table.'; + + ---------------------------------------------------------------------------------------- + -- Create new keys and indices + ---------------------------------------------------------------------------------------- + + -- add foreign key constraints + + ALTER TABLE survey_observation ADD CONSTRAINT survey_observation_fk2 + FOREIGN KEY (survey_sample_site_id) + REFERENCES survey_sample_site(survey_sample_site_id); + + ALTER TABLE survey_observation ADD CONSTRAINT survey_observation_fk3 + FOREIGN KEY (survey_sample_method_id) + REFERENCES survey_sample_method(survey_sample_method_id); + + ALTER TABLE survey_observation ADD CONSTRAINT survey_observation_fk4 + FOREIGN KEY (survey_sample_period_id) + REFERENCES survey_sample_period(survey_sample_period_id); + + -- add indexes for foreign keys + CREATE INDEX survey_observation_idx2 ON survey_observation(survey_sample_site_id); + + CREATE INDEX survey_observation_idx3 ON survey_observation(survey_sample_method_id); + + CREATE INDEX survey_observation_idx4 ON survey_observation(survey_sample_period_id); + + + + ---------------------------------------------------------------------------------------- + -- Create new views + ---------------------------------------------------------------------------------------- + + set search_path=biohub_dapi_v1; + + create or replace view survey_observation as select * from biohub.survey_observation; + + + + ---------------------------------------------------------------------------------------- + -- Update api_delete_survey procedure + ---------------------------------------------------------------------------------------- + + set search_path=biohub; + + CREATE OR REPLACE PROCEDURE api_delete_survey(p_survey_id integer) + LANGUAGE plpgsql + SECURITY DEFINER + AS $procedure$ + -- ******************************************************************* + -- Procedure: api_delete_survey + -- Purpose: deletes a survey and dependencies + -- + -- MODIFICATION HISTORY + -- Person Date Comments + -- ---------------- ----------- -------------------------------------- + -- shreyas.devalapurkar@quartech.com + -- 2021-06-18 initial release + -- charlie.garrettjones@quartech.com + -- 2021-06-21 added occurrence submission delete + -- charlie.garrettjones@quartech.com + -- 2021-09-21 added survey summary submission delete + -- kjartan.einarsson@quartech.com + -- 2022-08-28 added survey_vantage, survey_spatial_component, survey delete + -- charlie.garrettjones@quartech.com + -- 2022-09-07 changes to permit model + -- charlie.garrettjones@quartech.com + -- 2022-10-05 1.3.0 model changes + -- charlie.garrettjones@quartech.com + -- 2022-10-05 1.5.0 model changes, drop concept of occurrence deletion for published data + -- charlie.garrettjones@quartech.com + -- 2023-03-14 1.7.0 model changes + -- alfred.rosenthal@quartech.com + -- 2023-03-15 added missing publish tables to survey delete + -- curtis.upshall@quartech.com + -- 2023-04-28 change order of survey delete procedure + -- alfred.rosenthal@quartech.com + -- 2023-07-26 delete regions + -- curtis.upshall@quartech.com + -- 2023-08-24 delete partnerships + -- curtis.upshall@quartech.com + -- 2023-08-24 delete survey blocks and stratums and participation + -- curtis.upshall@quartech.com + -- 2023-09-25 delete survey observations and sampling sites, methods, periods + -- nick.phura@quartech.com + -- 2023-09-29 delete survey sampling sites, methods, periods + -- ******************************************************************* + declare + + begin + with occurrence_submissions as (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id), submission_spatial_components as (select submission_spatial_component_id from submission_spatial_component + where occurrence_submission_id in (select occurrence_submission_id from occurrence_submissions)) + delete from spatial_transform_submission where submission_spatial_component_id in (select submission_spatial_component_id from submission_spatial_components); + delete from submission_spatial_component where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + with occurrence_submissions as (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id) + , submission_statuses as (select submission_status_id from submission_status + where occurrence_submission_id in (select occurrence_submission_id from occurrence_submissions)) + delete from submission_message where submission_status_id in (select submission_status_id from submission_statuses); + delete from submission_status where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + delete from occurrence_submission_publish where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + delete from occurrence_submission where survey_id = p_survey_id; + + delete from survey_summary_submission_publish where survey_summary_submission_id in (select survey_summary_submission_id from survey_summary_submission where survey_id = p_survey_id); + delete from survey_summary_submission_message where survey_summary_submission_id in (select survey_summary_submission_id from survey_summary_submission where survey_id = p_survey_id); + delete from survey_summary_submission where survey_id = p_survey_id; + delete from survey_proprietor where survey_id = p_survey_id; + + -- delete survey attachments/reports + delete from survey_attachment_publish where survey_attachment_id in (select survey_attachment_id from survey_attachment where survey_id = p_survey_id); + delete from survey_attachment where survey_id = p_survey_id; + delete from survey_report_author where survey_report_attachment_id in (select survey_report_attachment_id from survey_report_attachment where survey_id = p_survey_id); + delete from survey_report_publish where survey_report_attachment_id in (select survey_report_attachment_id from survey_report_attachment where survey_id = p_survey_id); + delete from survey_report_attachment where survey_id = p_survey_id; + + delete from study_species where survey_id = p_survey_id; + delete from survey_funding_source where survey_id = p_survey_id; + delete from survey_vantage where survey_id = p_survey_id; + delete from survey_spatial_component where survey_id = p_survey_id; + delete from survey_metadata_publish where survey_id = p_survey_id; + delete from survey_region where survey_id = p_survey_id; + delete from survey_first_nation_partnership where survey_id = p_survey_id; + delete from survey_block where survey_id = p_survey_id; + delete from permit where survey_id = p_survey_id; + delete from survey_type where survey_id = p_survey_id; + delete from survey_first_nation_partnership where survey_id = p_survey_id; + delete from survey_stakeholder_partnership where survey_id = p_survey_id; + delete from survey_participation where survey_id = p_survey_id; + delete from survey_stratum where survey_id = p_survey_id; + delete from survey_block where survey_id = p_survey_id; + delete from survey_site_strategy where survey_id = p_survey_id; + + -- delete survey sample period/method/site + delete from survey_sample_period where survey_sample_method_id in ( + select survey_sample_method_id + from survey_sample_method + where survey_sample_site_id in ( + select survey_sample_site_id + from survey_sample_site + where survey_id = p_survey_id + ) + ); + delete from survey_sample_period where survey_sample_site_id in ( + select survey_sample_site_id + from survey_sample_site + where survey_id = p_survey_id + ); + delete from survey_sample_site where survey_id = p_survey_id; + + -- delete survey observations + delete from survey_observation where survey_id = p_survey_id; + + -- delete the survey + delete from survey where survey_id = p_survey_id; + + exception + when others THEN + raise; + end; + $procedure$; + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +}