diff --git a/api/src/models/survey-create.test.ts b/api/src/models/survey-create.test.ts index a51f56c6b1..6e98c35e4d 100644 --- a/api/src/models/survey-create.test.ts +++ b/api/src/models/survey-create.test.ts @@ -40,8 +40,8 @@ describe('PostSurveyObject', () => { expect(data.purpose_and_methodology).to.equal(null); }); - it('sets location', () => { - expect(data.location).to.equal(null); + it('sets locations', () => { + expect(data.locations).to.eql([]); }); it('sets agreements', () => { @@ -62,7 +62,6 @@ describe('PostSurveyObject', () => { permit: {}, proprietor: {}, purpose_and_methodology: {}, - location: {}, agreements: {} }; @@ -90,10 +89,6 @@ describe('PostSurveyObject', () => { expect(data.purpose_and_methodology).to.instanceOf(PostPurposeAndMethodologyData); }); - it('sets location', () => { - expect(data.location).to.instanceOf(PostLocationData); - }); - it('sets agreements', () => { expect(data.agreements).to.instanceOf(PostAgreementsData); }); @@ -393,12 +388,16 @@ describe('PostLocationData', () => { data = new PostLocationData(null); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(null); + it('sets name', () => { + expect(data.name).to.equal(null); + }); + + it('sets description', () => { + expect(data.description).to.equal(null); }); - it('sets geometry', () => { - expect(data.geometry).to.eql([]); + it('sets geojson', () => { + expect(data.geojson).to.eql([]); }); }); @@ -406,20 +405,25 @@ describe('PostLocationData', () => { let data: PostLocationData; const obj = { - survey_area_name: 'area_name', - geometry: [{}] + name: 'area name', + description: 'area description', + geojson: [{}] }; before(() => { data = new PostLocationData(obj); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(obj.survey_area_name); + it('sets name', () => { + expect(data.name).to.equal(obj.name); + }); + + it('sets description', () => { + expect(data.description).to.equal(obj.description); }); - it('sets geometry', () => { - expect(data.geometry).to.eql(obj.geometry); + it('sets geojson', () => { + expect(data.geojson).to.eql(obj.geojson); }); }); }); diff --git a/api/src/models/survey-create.ts b/api/src/models/survey-create.ts index fbbc381c2c..fd6359911d 100644 --- a/api/src/models/survey-create.ts +++ b/api/src/models/survey-create.ts @@ -9,7 +9,7 @@ export class PostSurveyObject { funding_sources: PostFundingSourceData[]; proprietor: PostProprietorData; purpose_and_methodology: PostPurposeAndMethodologyData; - location: PostLocationData; + locations: PostLocationData[]; agreements: PostAgreementsData; participants: PostParticipationData[]; partnerships: PostPartnershipsData; @@ -25,11 +25,11 @@ export class PostSurveyObject { this.proprietor = (obj?.proprietor && new PostProprietorData(obj.proprietor)) || null; this.purpose_and_methodology = (obj?.purpose_and_methodology && new PostPurposeAndMethodologyData(obj.purpose_and_methodology)) || null; - this.location = (obj?.location && new PostLocationData(obj.location)) || null; this.agreements = (obj?.agreements && new PostAgreementsData(obj.agreements)) || null; this.participants = (obj?.participants?.length && obj.participants.map((p: any) => new PostParticipationData(p))) || []; this.partnerships = (obj?.partnerships && new PostPartnershipsData(obj.partnerships)) || null; + this.locations = (obj?.locations && obj.locations.map((p: any) => new PostLocationData(p))) || []; this.site_selection = (obj?.site_selection && new PostSiteSelectionData(obj)) || null; this.blocks = (obj?.blocks && obj.blocks.map((p: any) => p as PostSurveyBlock)) || []; } @@ -125,12 +125,14 @@ export class PostProprietorData { } export class PostLocationData { - survey_area_name: string; - geometry: Feature[]; + name: string; + description: string; + geojson: Feature[]; constructor(obj?: any) { - this.survey_area_name = obj?.survey_area_name || null; - this.geometry = (obj?.geometry?.length && obj.geometry) || []; + this.name = obj?.name || null; + this.description = obj?.description || null; + this.geojson = (obj?.geojson?.length && obj.geojson) || []; } } diff --git a/api/src/models/survey-update.test.ts b/api/src/models/survey-update.test.ts index 4d3d1d5d6e..1195366e35 100644 --- a/api/src/models/survey-update.test.ts +++ b/api/src/models/survey-update.test.ts @@ -40,7 +40,7 @@ describe('PutSurveyObject', () => { }); it('sets location', () => { - expect(data.location).to.equal(null); + expect(data.locations).to.eql([]); }); }); @@ -53,7 +53,6 @@ describe('PutSurveyObject', () => { permit: {}, proprietor: {}, purpose_and_methodology: {}, - location: {}, agreements: {} }; @@ -80,10 +79,6 @@ describe('PutSurveyObject', () => { it('sets purpose_and_methodology', () => { expect(data.purpose_and_methodology).to.instanceOf(PutSurveyPurposeAndMethodologyData); }); - - it('sets location', () => { - expect(data.location).to.instanceOf(PutSurveyLocationData); - }); }); }); @@ -439,12 +434,16 @@ describe('PutLocationData', () => { data = new PutSurveyLocationData(null); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(null); + it('sets name', () => { + expect(data.name).to.equal(null); + }); + + it('sets description', () => { + expect(data.description).to.equal(null); }); - it('sets geometry', () => { - expect(data.geometry).to.eql([]); + it('sets geojson', () => { + expect(data.geojson).to.eql([]); }); it('sets revision_count', () => { @@ -456,8 +455,9 @@ describe('PutLocationData', () => { let data: PutSurveyLocationData; const obj = { - survey_area_name: 'area_name', - geometry: [{}], + name: 'area name', + description: 'area description', + geojson: [{}], revision_count: 0 }; @@ -465,12 +465,16 @@ describe('PutLocationData', () => { data = new PutSurveyLocationData(obj); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(obj.survey_area_name); + it('sets name', () => { + expect(data.name).to.equal(obj.name); + }); + + it('sets description', () => { + expect(data.description).to.equal(obj.description); }); - it('sets geometry', () => { - expect(data.geometry).to.eql(obj.geometry); + it('sets geojson', () => { + expect(data.geojson).to.eql(obj.geojson); }); it('sets revision_count', () => { diff --git a/api/src/models/survey-update.ts b/api/src/models/survey-update.ts index 255a5cf08d..c57501bc2c 100644 --- a/api/src/models/survey-update.ts +++ b/api/src/models/survey-update.ts @@ -9,7 +9,7 @@ export class PutSurveyObject { funding_sources: PutFundingSourceData[]; proprietor: PutSurveyProprietorData; purpose_and_methodology: PutSurveyPurposeAndMethodologyData; - location: PutSurveyLocationData; + locations: PutSurveyLocationData[]; participants: PutSurveyParticipantsData[]; partnerships: PutPartnershipsData; site_selection: PutSiteSelectionData; @@ -24,9 +24,9 @@ export class PutSurveyObject { this.proprietor = (obj?.proprietor && new PutSurveyProprietorData(obj.proprietor)) || null; this.purpose_and_methodology = (obj?.purpose_and_methodology && new PutSurveyPurposeAndMethodologyData(obj.purpose_and_methodology)) || null; - this.location = (obj?.location && new PutSurveyLocationData(obj.location)) || null; this.participants = (obj?.participants?.length && obj.participants.map((p: any) => new PutSurveyParticipantsData(p))) || []; + this.locations = (obj?.locations && obj.locations.map((p: any) => new PutSurveyLocationData(p))) || []; this.partnerships = (obj?.partnerships && new PutPartnershipsData(obj.partnerships)) || null; this.site_selection = (obj?.site_selection && new PutSiteSelectionData(obj)) || null; this.blocks = (obj?.blocks && obj.blocks.map((p: any) => p as PostSurveyBlock)) || []; @@ -155,13 +155,17 @@ export class PutSurveyPurposeAndMethodologyData { } export class PutSurveyLocationData { - survey_area_name: string; - geometry: Feature[]; + survey_location_id: number; + name: string; + description: string; + geojson: Feature[]; revision_count: number; constructor(obj?: any) { - this.survey_area_name = obj?.survey_area_name || null; - this.geometry = (obj?.geometry?.length && obj.geometry) || []; + this.survey_location_id = obj?.survey_location_id || null; + this.name = obj?.name || null; + this.description = obj?.description || null; + this.geojson = (obj?.geojson?.length && obj.geojson) || []; this.revision_count = obj?.revision_count ?? null; } } diff --git a/api/src/models/survey-view.test.ts b/api/src/models/survey-view.test.ts index 573d2cc730..ef989dbfaa 100644 --- a/api/src/models/survey-view.test.ts +++ b/api/src/models/survey-view.test.ts @@ -306,12 +306,16 @@ describe('GetSurveyLocationData', () => { data = new GetSurveyLocationData(null); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(''); + it('sets name', () => { + expect(data.name).to.equal(null); }); - it('sets geometry', () => { - expect(data.geometry).to.eql([]); + it('sets description', () => { + expect(data.description).to.equal(null); + }); + + it('sets geojson', () => { + expect(data.geojson).to.eql([]); }); }); @@ -319,20 +323,25 @@ describe('GetSurveyLocationData', () => { let data: GetSurveyLocationData; const obj = { - location_name: 'area_name', - geojson: [{}] + name: 'area name', + description: 'area description', + geojson: [] }; before(() => { data = new GetSurveyLocationData(obj); }); - it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(obj.location_name); + it('sets name', () => { + expect(data.name).to.equal(obj.name); }); - it('sets geometry', () => { - expect(data.geometry).to.eql(obj.geojson); + it('sets description', () => { + expect(data.description).to.equal(obj.description); + }); + + it('sets geojson', () => { + expect(data.geojson).to.eql(obj.geojson); }); }); }); diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index d0aab6ac01..655eb23b99 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -3,6 +3,7 @@ import { SurveyMetadataPublish } from '../repositories/history-publish-repositor import { IPermitModel } from '../repositories/permit-repository'; import { SiteSelectionData } from '../repositories/site-selection-strategy-repository'; import { SurveyBlockRecord } from '../repositories/survey-block-repository'; +import { SurveyLocationRecord } from '../repositories/survey-location-repository'; import { SurveyUser } from '../repositories/survey-participation-repository'; export type SurveyObject = { @@ -12,7 +13,7 @@ export type SurveyObject = { funding_sources: GetSurveyFundingSourceData[]; purpose_and_methodology: GetSurveyPurposeAndMethodologyData; proprietor: GetSurveyProprietorData | null; - location: GetSurveyLocationData; + locations: SurveyLocationRecord[]; participants: SurveyUser[]; partnerships: ISurveyPartnerships; site_selection: SiteSelectionData; @@ -33,22 +34,22 @@ export class GetSurveyData { end_date: string; biologist_first_name: string; biologist_last_name: string; - survey_area_name: string; - geometry: Feature[]; survey_types: number[]; revision_count: number; + geometry: Feature[]; constructor(obj?: any) { this.id = obj?.survey_id || null; this.project_id = obj?.project_id || null; this.uuid = obj?.uuid || null; this.survey_name = obj?.name || ''; + this.start_date = obj?.start_date || null; + this.end_date = obj?.end_date || null; this.start_date = String(obj?.start_date) || ''; this.end_date = String(obj?.end_date) || ''; this.geometry = (obj?.geojson?.length && obj.geojson) || []; this.biologist_first_name = obj?.lead_first_name || ''; this.biologist_last_name = obj?.lead_last_name || ''; - this.survey_area_name = obj?.location_name || ''; this.survey_types = (obj?.survey_types?.length && obj.survey_types) || []; this.revision_count = obj?.revision_count || 0; } @@ -169,12 +170,18 @@ export type SurveySupplementaryData = { }; export class GetSurveyLocationData { - survey_area_name: string; - geometry: Feature[]; + survey_spatial_component_id: number; + name: string; + description: string; + geojson: Feature[]; + revision_count: number; constructor(obj?: any) { - this.survey_area_name = obj?.location_name || ''; - this.geometry = (obj?.geojson?.length && obj.geojson) || []; + this.survey_spatial_component_id = obj?.survey_spatial_component_id || null; + this.name = obj?.name || null; + this.description = obj?.description || null; + this.geojson = (obj?.geojson?.length && obj.geojson) || []; + this.revision_count = obj?.revision_count || 0; } } diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 71b8de12c5..bdf246153b 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -63,8 +63,8 @@ POST.apiDoc = { 'partnerships', 'proprietor', 'purpose_and_methodology', + 'locations', 'site_selection', - 'location', 'agreements', 'participants' ], @@ -220,6 +220,30 @@ POST.apiDoc = { } } }, + locations: { + description: 'Survey location data', + type: 'array', + items: { + type: 'object', + required: ['name', 'description', 'geojson'], + properties: { + name: { + type: 'string', + maxLength: 100 + }, + description: { + type: 'string', + maxLength: 250 + }, + geojson: { + type: 'array', + items: { + ...(GeoJSONFeature as object) + } + } + } + } + }, site_selection: { type: 'object', required: ['strategies', 'stratums'], @@ -247,20 +271,6 @@ POST.apiDoc = { } } }, - location: { - type: 'object', - properties: { - survey_area_name: { - type: 'string' - }, - geometry: { - type: 'array', - items: { - ...(GeoJSONFeature as object) - } - } - } - }, participants: { type: 'array', items: { @@ -345,7 +355,6 @@ export function createSurvey(): RequestHandler { await connection.open(); const surveyService = new SurveyService(connection); - const surveyId = await surveyService.createSurveyAndUploadMetadataToBioHub(projectId, sanitizedPostSurveyData); await connection.commit(); diff --git a/api/src/paths/project/{projectId}/survey/list.ts b/api/src/paths/project/{projectId}/survey/list.ts index 852b6a4c6c..0b5570bab6 100644 --- a/api/src/paths/project/{projectId}/survey/list.ts +++ b/api/src/paths/project/{projectId}/survey/list.ts @@ -71,7 +71,7 @@ GET.apiDoc = { 'permit', 'proprietor', 'purpose_and_methodology', - 'location' + 'locations' ], properties: { survey_details: { @@ -253,18 +253,49 @@ GET.apiDoc = { } } }, - location: { - description: 'Survey location Details', - type: 'object', - required: ['survey_area_name', 'geometry'], - properties: { - survey_area_name: { - type: 'string' - }, - geometry: { - type: 'array', - items: { - ...(GeoJSONFeature as object) + locations: { + description: 'Survey location data', + type: 'array', + items: { + type: 'object', + required: [ + 'survey_location_id', + 'name', + 'description', + 'geometry', + 'geography', + 'geojson', + 'revision_count' + ], + properties: { + survey_location_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string', + maxLength: 100 + }, + description: { + type: 'string', + maxLength: 250 + }, + geometry: { + type: 'string', + nullable: true + }, + geography: { + type: 'string' + }, + geojson: { + type: 'array', + items: { + ...(GeoJSONFeature as object) + } + }, + revision_count: { + type: 'integer', + minimum: 0 } } } diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments.ts index 1ac709da11..68d825357b 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments.ts @@ -243,7 +243,6 @@ export function deployDevice(): RequestHandler { return res.status(201).json(surveyEntry); } catch (error) { defaultLog.error({ label: 'addDeployment', message: 'error', error }); - console.log(JSON.stringify((error as Error).message)); await connection.rollback(); return res.status(500).json((error as AxiosError).response); } finally { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index 048fcf21f0..d5080e4985 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -64,17 +64,6 @@ PUT.apiDoc = { schema: { title: 'SurveyProject put request object', type: 'object', - required: [ - 'survey_details', - 'species', - 'permit', - 'funding_sources', - 'partnerships', - 'proprietor', - 'purpose_and_methodology', - 'site_selection', - 'location' - ], properties: { survey_details: { type: 'object', @@ -267,6 +256,38 @@ PUT.apiDoc = { } } }, + locations: { + description: 'Survey location data', + type: 'array', + items: { + type: 'object', + required: ['name', 'description', 'geojson'], + properties: { + survey_location_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string', + maxLength: 100 + }, + description: { + type: 'string', + maxLength: 250 + }, + geojson: { + type: 'array', + items: { + ...(GeoJSONFeature as object) + } + }, + revision_count: { + type: 'integer', + minimum: 0 + } + } + } + }, site_selection: { type: 'object', required: ['strategies', 'stratums'], @@ -294,24 +315,6 @@ PUT.apiDoc = { } } }, - location: { - type: 'object', - required: ['survey_area_name', 'geometry'], - properties: { - survey_area_name: { - type: 'string' - }, - geometry: { - type: 'array', - items: { - ...(GeoJSONFeature as object) - } - }, - revision_count: { - type: 'number' - } - } - }, participants: { type: 'array', items: { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts index 6888f4f60f..30b006c57e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts @@ -79,7 +79,7 @@ GET.apiDoc = { 'funding_sources', 'proprietor', 'purpose_and_methodology', - 'location', + 'locations', 'participants' ], properties: { @@ -301,18 +301,49 @@ GET.apiDoc = { } } }, - location: { - description: 'Survey location Details', - type: 'object', - required: ['survey_area_name', 'geometry'], - properties: { - survey_area_name: { - type: 'string' - }, - geometry: { - type: 'array', - items: { - ...(GeoJSONFeature as object) + locations: { + description: 'Survey location data', + type: 'array', + items: { + type: 'object', + required: [ + 'survey_location_id', + 'name', + 'description', + 'geometry', + 'geography', + 'geojson', + 'revision_count' + ], + properties: { + survey_location_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string', + maxLength: 100 + }, + description: { + type: 'string', + maxLength: 250 + }, + geometry: { + type: 'string', + nullable: true + }, + geography: { + type: 'string' + }, + geojson: { + type: 'array', + items: { + ...(GeoJSONFeature as object) + } + }, + revision_count: { + type: 'integer', + minimum: 0 } } } diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts index 4cedaa982e..a65e98bf68 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts @@ -62,13 +62,20 @@ describe('survey/{surveyId}/view', () => { indigenous_partnerships: [], stakeholder_partnerships: [] }, + locations: [ + { + survey_location_id: 1, + name: 'location name', + description: 'location description', + geometry: '', + geography: '', + geojson: [], + revision_count: 0 + } + ], site_selection: { strategies: ['strat1'], stratums: [{ name: 'startum1', description: 'desc' }] - }, - location: { - survey_area_name: 'location', - geometry: [] } }, surveySupplementaryData: { @@ -128,13 +135,20 @@ describe('survey/{surveyId}/view', () => { indigenous_partnerships: [], stakeholder_partnerships: [] }, + locations: [ + { + survey_location_id: 1, + name: 'location name', + description: 'location description', + geometry: null, + geography: '', + geojson: [], + revision_count: 0 + } + ], site_selection: { strategies: ['strat1'], stratums: [{ name: 'startum1', description: null }] - }, - location: { - survey_area_name: 'location', - geometry: [] } }, surveySupplementaryData: { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index 7f3957f817..de3a41b6d7 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -80,8 +80,8 @@ GET.apiDoc = { 'partnerships', 'proprietor', 'purpose_and_methodology', - 'site_selection', - 'location' + 'locations', + 'site_selection' ], properties: { survey_details: { @@ -314,6 +314,53 @@ GET.apiDoc = { } } }, + locations: { + description: 'Survey location data', + type: 'array', + items: { + type: 'object', + required: [ + 'survey_location_id', + 'name', + 'description', + 'geometry', + 'geography', + 'geojson', + 'revision_count' + ], + properties: { + survey_location_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string', + maxLength: 100 + }, + description: { + type: 'string', + maxLength: 250 + }, + geometry: { + type: 'string', + nullable: true + }, + geography: { + type: 'string' + }, + geojson: { + type: 'array', + items: { + ...(GeoJSONFeature as object) + } + }, + revision_count: { + type: 'integer', + minimum: 0 + } + } + } + }, site_selection: { type: 'object', required: ['strategies', 'stratums'], @@ -341,22 +388,6 @@ GET.apiDoc = { } } } - }, - location: { - description: 'Survey location Details', - type: 'object', - required: ['survey_area_name', 'geometry'], - properties: { - survey_area_name: { - type: 'string' - }, - geometry: { - type: 'array', - items: { - ...(GeoJSONFeature as object) - } - } - } } } }, diff --git a/api/src/repositories/administrative-activity-repository.ts b/api/src/repositories/administrative-activity-repository.ts index 7e20d6609a..5c4e47cd13 100644 --- a/api/src/repositories/administrative-activity-repository.ts +++ b/api/src/repositories/administrative-activity-repository.ts @@ -5,16 +5,9 @@ import { ADMINISTRATIVE_ACTIVITY_TYPE } from '../constants/administrative-activity'; import { ApiExecuteSQLError } from '../errors/api-error'; +import { jsonSchema } from '../zod-schema/json'; import { BaseRepository } from './base-repository'; -// Defines a Zod Schema for a valid JSON value -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -type Literal = z.infer; -type Json = Literal | { [key: string]: Json } | Json[]; -export const JsonSchema: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]) -); - export const IAdministrativeActivityStanding = z.object({ has_pending_acccess_request: z.boolean(), has_one_or_more_project_roles: z.boolean() @@ -29,7 +22,7 @@ export const IAdministrativeActivity = z.object({ status: z.number(), status_name: z.string(), description: z.string().nullable(), - data: JsonSchema, + data: jsonSchema, notes: z.string().nullable(), create_date: z.string() }); diff --git a/api/src/repositories/site-selection-strategy-repository.ts b/api/src/repositories/site-selection-strategy-repository.ts index f45ddc3535..f0d5443105 100644 --- a/api/src/repositories/site-selection-strategy-repository.ts +++ b/api/src/repositories/site-selection-strategy-repository.ts @@ -41,7 +41,7 @@ const defaultLog = getLogger('repositories/site-selection-strategy-repository'); */ export class SiteSelectionStrategyRepository extends BaseRepository { /** - * Retreives the site selection strategies and stratums for the given survey + * Retrieves the site selection strategies and stratums for the given survey * * @param {number} surveyId * @return {*} {Promise} @@ -53,7 +53,7 @@ export class SiteSelectionStrategyRepository extends BaseRepository { const strategiesQuery = getKnex() .select('ss.name') .from('survey_site_strategy as sss') - .where('sss.survey_id', 1) + .where('sss.survey_id', surveyId) .leftJoin('site_strategy as ss', 'ss.site_strategy_id', 'sss.site_strategy_id'); const strategiesResponse = await this.connection.knex<{ name: string }>(strategiesQuery); diff --git a/api/src/repositories/survey-location-repository.ts b/api/src/repositories/survey-location-repository.ts new file mode 100644 index 0000000000..67616bb327 --- /dev/null +++ b/api/src/repositories/survey-location-repository.ts @@ -0,0 +1,97 @@ +import SQL from 'sql-template-strings'; +import { z } from 'zod'; +import { PostLocationData } from '../models/survey-create'; +import { PutSurveyLocationData } from '../models/survey-update'; +import { generateGeometryCollectionSQL } from '../utils/spatial-utils'; +import { jsonSchema } from '../zod-schema/json'; +import { BaseRepository } from './base-repository'; + +export const SurveyLocationRecord = z.object({ + survey_location_id: z.number(), + name: z.string(), + description: z.string(), + geometry: z.record(z.any()).nullable(), + geography: z.string(), + geojson: jsonSchema, + revision_count: z.number() +}); + +export type SurveyLocationRecord = z.infer; +export class SurveyLocationRepository extends BaseRepository { + /** + * Creates a survey location for a given survey + * + * @param {number} surveyId + * @param {PostLocationData} data + * @memberof SurveyLocationRepository + */ + async insertSurveyLocation(surveyId: number, data: PostLocationData): Promise { + const sqlStatement = SQL` + INSERT INTO survey_location ( + survey_id, + name, + description, + geojson, + geography + ) + VALUES ( + ${surveyId}, + ${data.name}, + ${data.description}, + ${JSON.stringify(data.geojson)}, + public.geography( + public.ST_Force2D( + public.ST_SetSRID(`.append(generateGeometryCollectionSQL(data.geojson)).append(`, 4326) + ) + ) + );`); + await this.connection.sql(sqlStatement); + } + + /** + * Updates survey location data + * + * @param {PutSurveyLocationData} data + * @memberof SurveyLocationRepository + */ + async updateSurveyLocation(data: PutSurveyLocationData): Promise { + const sqlStatement = SQL` + UPDATE + survey_location + SET + name = ${data.name}, + description = ${data.description}, + geojson = ${JSON.stringify(data.geojson)}, + geography = public.geography( + public.ST_Force2D( + public.ST_SetSRID(`.append(generateGeometryCollectionSQL(data.geojson)).append(`, 4326) + ) + ) + WHERE + survey_location_id = ${data.survey_location_id}; + `); + + await this.connection.sql(sqlStatement); + } + + /** + * Get Survey location for a given survey ID + * + * @param {number} surveyId + * @returns {*} Promise + * @memberof SurveyLocationRepository + */ + async getSurveyLocationsData(surveyId: number): Promise { + const sqlStatement = SQL` + SELECT + * + FROM + survey_location + WHERE + survey_id = ${surveyId}; + `; + + const response = await this.connection.sql(sqlStatement, SurveyLocationRecord); + return response.rows; + } +} diff --git a/api/src/repositories/survey-repository.test.ts b/api/src/repositories/survey-repository.test.ts index fa716a5efa..4f4850b4d0 100644 --- a/api/src/repositories/survey-repository.test.ts +++ b/api/src/repositories/survey-repository.test.ts @@ -6,12 +6,7 @@ import sinonChai from 'sinon-chai'; import { GetReportAttachmentsData } from '../models/project-view'; import { PostProprietorData, PostSurveyObject } from '../models/survey-create'; import { PutSurveyObject } from '../models/survey-update'; -import { - GetAttachmentsData, - GetSurveyLocationData, - GetSurveyProprietorData, - GetSurveyPurposeAndMethodologyData -} from '../models/survey-view'; +import { GetAttachmentsData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData } from '../models/survey-view'; import { getMockDBConnection } from '../__mocks__/db'; import { IObservationSubmissionInsertDetails, @@ -198,19 +193,6 @@ describe('SurveyRepository', () => { }); }); - describe('getSurveyLocationData', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ sql: () => mockResponse }); - - const repository = new SurveyRepository(dbConnection); - - const response = await repository.getSurveyLocationData(1); - - expect(response).to.eql(new GetSurveyLocationData({ id: 1 })); - }); - }); - describe('getStakeholderPartnershipsBySurveyId', () => { it('should return stakeholder partnerships', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; @@ -492,7 +474,7 @@ describe('SurveyRepository', () => { intended_outcome_id: 1, surveyed_all_areas: 'Y' }, - location: { geometry: [{ id: 1 }] } + locations: [{ geometry: [{ id: 1 }] }] } as unknown) as PostSurveyObject; const response = await repository.insertSurveyData(1, input); @@ -522,7 +504,7 @@ describe('SurveyRepository', () => { intended_outcome_id: 1, surveyed_all_areas: 'Y' }, - location: { geometry: [] } + locations: [{ geometry: [] }] } as unknown) as PostSurveyObject; const response = await repository.insertSurveyData(1, input); @@ -552,7 +534,7 @@ describe('SurveyRepository', () => { intended_outcome_id: 1, surveyed_all_areas: 'Y' }, - location: { geometry: [{ id: 1 }] } + locations: [{ geometry: [{ id: 1 }] }] } as unknown) as PostSurveyObject; try { @@ -881,7 +863,7 @@ describe('SurveyRepository', () => { surveyed_all_areas: 'Y', revision_count: 1 }, - location: { geometry: [{ id: 1 }] } + locations: [{ geometry: [{ id: 1 }] }] } as unknown) as PutSurveyObject; const response = await repository.updateSurveyDetailsData(1, input); @@ -912,7 +894,7 @@ describe('SurveyRepository', () => { surveyed_all_areas: 'Y', revision_count: 1 }, - location: { geometry: [] } + locations: [{ geometry: [] }] } as unknown) as PutSurveyObject; const response = await repository.updateSurveyDetailsData(1, input); @@ -943,7 +925,7 @@ describe('SurveyRepository', () => { surveyed_all_areas: 'Y', revision_count: 1 }, - location: { geometry: [] } + locations: [{ geometry: [] }] } as unknown) as PutSurveyObject; try { diff --git a/api/src/repositories/survey-repository.ts b/api/src/repositories/survey-repository.ts index df47547bbb..b1b1d971b0 100644 --- a/api/src/repositories/survey-repository.ts +++ b/api/src/repositories/survey-repository.ts @@ -8,12 +8,10 @@ import { PutSurveyObject } from '../models/survey-update'; import { GetAttachmentsData, GetReportAttachmentsData, - GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData } from '../models/survey-view'; import { getLogger } from '../utils/logger'; -import { generateGeometryCollectionSQL } from '../utils/spatial-utils'; import { BaseRepository } from './base-repository'; export interface IGetSpeciesData { @@ -87,11 +85,6 @@ const SurveyRecord = z.object({ additional_details: z.string().nullable(), ecological_season_id: z.number().nullable(), intended_outcome_id: z.number().nullable(), - location_name: z.string(), - location_description: z.string().nullable(), - geometry: z.any().nullable(), - geography: z.any().nullable(), - geojson: z.any().nullable(), comments: z.string().nullable(), create_date: z.string(), create_user: z.number(), @@ -347,30 +340,6 @@ export class SurveyRepository extends BaseRepository { return new GetSurveyProprietorData(result); } - /** - * Get Survey location for a given survey ID - * - * @param {number} surveyId - * @returns {*} Promise - * @memberof SurveyRepository - */ - async getSurveyLocationData(surveyId: number): Promise { - const sqlStatement = SQL` - SELECT - * - FROM - survey - WHERE - survey_id = ${surveyId}; - `; - - const response = await this.connection.sql(sqlStatement); - - const result = response.rows?.[0]; - - return new GetSurveyLocationData(result); - } - /** * Get Occurrence submission for a given survey id. * @@ -619,10 +588,7 @@ export class SurveyRepository extends BaseRepository { field_method_id, additional_details, ecological_season_id, - intended_outcome_id, - location_name, - geojson, - geography + intended_outcome_id ) VALUES ( ${projectId}, ${surveyData.survey_details.survey_name}, @@ -633,39 +599,13 @@ export class SurveyRepository extends BaseRepository { ${surveyData.purpose_and_methodology.field_method_id}, ${surveyData.purpose_and_methodology.additional_details}, ${surveyData.purpose_and_methodology.ecological_season_id}, - ${surveyData.purpose_and_methodology.intended_outcome_id}, - ${surveyData.location.survey_area_name}, - ${JSON.stringify(surveyData.location.geometry)} - `; - - if (surveyData?.location?.geometry?.length) { - const geometryCollectionSQL = generateGeometryCollectionSQL(surveyData.location.geometry); - - sqlStatement.append(SQL` - ,public.geography( - public.ST_Force2D( - public.ST_SetSRID( - `); - - sqlStatement.append(geometryCollectionSQL); - - sqlStatement.append(SQL` - , 4326))) - `); - } else { - sqlStatement.append(SQL` - ,null - `); - } - - sqlStatement.append(SQL` + ${surveyData.purpose_and_methodology.intended_outcome_id} ) RETURNING survey_id as id; - `); + `; const response = await this.connection.sql(sqlStatement); - const result = response.rows?.[0]; if (!result) { @@ -953,8 +893,7 @@ export class SurveyRepository extends BaseRepository { start_date: surveyData.survey_details.start_date, end_date: surveyData.survey_details.end_date, lead_first_name: surveyData.survey_details.lead_first_name, - lead_last_name: surveyData.survey_details.lead_last_name, - revision_count: surveyData.survey_details.revision_count + lead_last_name: surveyData.survey_details.lead_last_name }; } @@ -964,43 +903,14 @@ export class SurveyRepository extends BaseRepository { field_method_id: surveyData.purpose_and_methodology.field_method_id, additional_details: surveyData.purpose_and_methodology.additional_details, ecological_season_id: surveyData.purpose_and_methodology.ecological_season_id, - intended_outcome_id: surveyData.purpose_and_methodology.intended_outcome_id, - revision_count: surveyData.purpose_and_methodology.revision_count - }; - } - - if (surveyData.location) { - const geometrySqlStatement = SQL``; - - if (surveyData?.location?.geometry?.length) { - geometrySqlStatement.append(SQL` - public.geography( - public.ST_Force2D( - public.ST_SetSRID( - `); - - const geometryCollectionSQL = generateGeometryCollectionSQL(surveyData.location.geometry); - geometrySqlStatement.append(geometryCollectionSQL); - - geometrySqlStatement.append(SQL` - , 4326))) - `); - } else { - geometrySqlStatement.append(SQL` - null - `); - } - - fieldsToUpdate = { - ...fieldsToUpdate, - location_name: surveyData.location.survey_area_name, - geojson: JSON.stringify(surveyData.location.geometry), - geography: knex.raw(geometrySqlStatement.sql, geometrySqlStatement.values), - revision_count: surveyData.location.revision_count + intended_outcome_id: surveyData.purpose_and_methodology.intended_outcome_id }; } - const updateSurveyQueryBuilder = knex('survey').update(fieldsToUpdate).where('survey_id', surveyId); + const updateSurveyQueryBuilder = knex('survey') + .update(fieldsToUpdate) + .where('survey_id', surveyId) + .andWhere('revision_count', surveyData.survey_details.revision_count); const result = await this.connection.knex(updateSurveyQueryBuilder); diff --git a/api/src/services/eml-service.ts b/api/src/services/eml-service.ts index a7ec97b5ec..813287244b 100644 --- a/api/src/services/eml-service.ts +++ b/api/src/services/eml-service.ts @@ -908,17 +908,19 @@ export class EmlService extends DBService { * @memberof EmlService */ _getSurveyGeographicCoverage(surveyData: SurveyObject): Record { - if (!surveyData.location.geometry?.length) { + if (!surveyData.locations[0]?.geometry?.length) { return {}; } - const polygonFeatures = this._makePolygonFeatures(surveyData.location.geometry); + const polygonFeatures = this._makePolygonFeatures( + surveyData.locations[0].geometry as Feature[] + ); const datasetGPolygons = this._makeDatasetGPolygons(polygonFeatures); const surveyBoundingBox = bbox(featureCollection(polygonFeatures)); return { geographicCoverage: { - geographicDescription: surveyData.location.survey_area_name, + geographicDescription: surveyData.locations[0].name, boundingCoordinates: { westBoundingCoordinate: surveyBoundingBox[0], eastBoundingCoordinate: surveyBoundingBox[2], diff --git a/api/src/services/project-service.ts b/api/src/services/project-service.ts index dcc70fdbe8..36a6f5e8c2 100644 --- a/api/src/services/project-service.ts +++ b/api/src/services/project-service.ts @@ -269,23 +269,63 @@ export class ProjectService extends DBService { return projectId; } + /** + * Insert project data. + * + * @param {PostProjectObject} postProjectData + * @return {*} {Promise} + * @memberof ProjectService + */ async insertProject(postProjectData: PostProjectObject): Promise { return this.projectRepository.insertProject(postProjectData); } + /** + * Insert IUCN classification data. + * + * @param {number} iucn3_id + * @param {number} project_id + * @return {*} {Promise} + * @memberof ProjectService + */ async insertClassificationDetail(iucn3_id: number, project_id: number): Promise { return this.projectRepository.insertClassificationDetail(iucn3_id, project_id); } + /** + * Insert participation data. + * + * @param {number} projectId + * @param {number} systemUserId + * @param {string} projectParticipantRole + * @return {*} {Promise} + * @memberof ProjectService + */ async postProjectParticipant(projectId: number, systemUserId: number, projectParticipantRole: string): Promise { return this.projectParticipationService.postProjectParticipant(projectId, systemUserId, projectParticipantRole); } + /** + * Insert region data. + * + * @param {number} projectId + * @param {Feature[]} features + * @return {*} {Promise} + * @memberof ProjectService + */ async insertRegion(projectId: number, features: Feature[]): Promise { const regionService = new RegionService(this.connection); return regionService.addRegionsToProjectFromFeatures(projectId, features); } + /** + * Insert programs data. + * + * @param {number} projectId + * @param {number[]} projectPrograms + * @return {*} {Promise} + * @memberof ProjectService + */ async insertPrograms(projectId: number, projectPrograms: number[]): Promise { await this.projectRepository.deletePrograms(projectId); await this.projectRepository.insertProgram(projectId, projectPrograms); diff --git a/api/src/services/survey-location-service.ts b/api/src/services/survey-location-service.ts new file mode 100644 index 0000000000..296794a042 --- /dev/null +++ b/api/src/services/survey-location-service.ts @@ -0,0 +1,61 @@ +import { IDBConnection } from '../database/db'; +import { PostLocationData } from '../models/survey-create'; +import { PutSurveyLocationData } from '../models/survey-update'; +import { SurveyLocationRecord, SurveyLocationRepository } from '../repositories/survey-location-repository'; +import { DBService } from './db-service'; + +/** + * Service for reading/writing survey location data. + * + * @export + * @class SurveyLocationService + * @extends {DBService} + */ +export class SurveyLocationService extends DBService { + surveyLocationRepository: SurveyLocationRepository; + + /** + * Creates an instance of SurveyLocationService. + * + * @param {IDBConnection} connection + * @memberof SurveyLocationService + */ + constructor(connection: IDBConnection) { + super(connection); + + this.surveyLocationRepository = new SurveyLocationRepository(connection); + } + + /** + * Insert a new survey location record. + * + * @param {number} surveyId + * @param {PostLocationData} data + * @return {*} {Promise} + * @memberof SurveyLocationService + */ + async insertSurveyLocation(surveyId: number, data: PostLocationData): Promise { + return this.surveyLocationRepository.insertSurveyLocation(surveyId, data); + } + + /** + * Update an existing survey location record. + * + * @param {PutSurveyLocationData} data + * @return {*} {Promise} + * @memberof SurveyLocationService + */ + async updateSurveyLocation(data: PutSurveyLocationData): Promise { + return this.surveyLocationRepository.updateSurveyLocation(data); + } + /** + * Get survey location records for a given survey ID + * + * @param {number} surveyID + * @returns {*} {Promise} + * @memberof SurveyLocationService + */ + async getSurveyLocationsData(surveyId: number): Promise { + return this.surveyLocationRepository.getSurveyLocationsData(surveyId); + } +} diff --git a/api/src/services/survey-service.test.ts b/api/src/services/survey-service.test.ts index ba842d423a..c076835879 100644 --- a/api/src/services/survey-service.test.ts +++ b/api/src/services/survey-service.test.ts @@ -13,7 +13,6 @@ import { GetAttachmentsData, GetFocalSpeciesData, GetSurveyData, - GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData, SurveyObject @@ -21,6 +20,7 @@ import { import { FundingSourceRepository } from '../repositories/funding-source-repository'; import { PublishStatus } from '../repositories/history-publish-repository'; import { IPermitModel } from '../repositories/permit-repository'; +import { SurveyLocationRecord, SurveyLocationRepository } from '../repositories/survey-location-repository'; import { IGetLatestSurveyOccurrenceSubmission, IGetSpeciesData, @@ -74,9 +74,7 @@ describe('SurveyService', () => { const getSurveyProprietorDataForViewStub = sinon .stub(SurveyService.prototype, 'getSurveyProprietorDataForView') .resolves(({ data: 'proprietorData' } as unknown) as any); - const getSurveyLocationDataStub = sinon - .stub(SurveyService.prototype, 'getSurveyLocationData') - .resolves(({ data: 'locationData' } as unknown) as any); + const getSurveyLocationsDataStub = sinon.stub(SurveyService.prototype, 'getSurveyLocationsData').resolves([]); const getSurveyParticipantsStub = sinon .stub(SurveyParticipationService.prototype, 'getSurveyParticipants') .resolves([{ data: 'participantData' } as any]); @@ -98,7 +96,7 @@ describe('SurveyService', () => { expect(getSurveyFundingSourceDataStub).to.be.calledOnce; expect(getSurveyPurposeAndMethodologyStub).to.be.calledOnce; expect(getSurveyProprietorDataForViewStub).to.be.calledOnce; - expect(getSurveyLocationDataStub).to.be.calledOnce; + expect(getSurveyLocationsDataStub).to.be.calledOnce; expect(getSurveyParticipantsStub).to.be.calledOnce; expect(getSurveyPartnershipsDataStub).to.be.calledOnce; expect(getSurveyBlockStub).to.be.calledOnce; @@ -116,7 +114,7 @@ describe('SurveyService', () => { stakeholder_partnerships: [] }, participants: [{ data: 'participantData' } as any], - location: { data: 'locationData' }, + locations: [], site_selection: { stratums: [], strategies: [] }, blocks: [] }); @@ -186,7 +184,6 @@ describe('SurveyService', () => { const updateSurveyProprietorDataStub = sinon .stub(SurveyService.prototype, 'updateSurveyProprietorData') .resolves(); - const updateSurveyRegionStub = sinon.stub(SurveyService.prototype, 'insertRegion').resolves(); const upsertSurveyParticipantDataStub = sinon .stub(SurveyService.prototype, 'upsertSurveyParticipantData') .resolves(); @@ -208,9 +205,9 @@ describe('SurveyService', () => { funding_sources: [{}], proprietor: {}, purpose_and_methodology: {}, - location: {}, - site_selection: { stratums: [], strategies: [] }, + locations: [], participants: [{}], + site_selection: { stratums: [], strategies: [] }, blocks: [{}] }); @@ -223,7 +220,6 @@ describe('SurveyService', () => { expect(updateSurveyPermitDataStub).to.have.been.calledOnce; expect(upsertSurveyFundingSourceDataStub).to.have.been.calledOnce; expect(updateSurveyProprietorDataStub).to.have.been.calledOnce; - expect(updateSurveyRegionStub).to.have.been.calledOnce; expect(upsertSurveyParticipantDataStub).to.have.been.calledOnce; expect(upsertBlocks).to.have.been.calledOnce; expect(replaceSurveyStratumsStub).to.have.been.calledOnce; @@ -458,11 +454,11 @@ describe('SurveyService', () => { const dbConnection = getMockDBConnection(); const service = new SurveyService(dbConnection); - const data = new GetSurveyLocationData([{ id: 1 }]); + const data = ([{ survey_location_id: 1 }] as any) as SurveyLocationRecord[]; - const repoStub = sinon.stub(SurveyRepository.prototype, 'getSurveyLocationData').resolves(data); + const repoStub = sinon.stub(SurveyLocationRepository.prototype, 'getSurveyLocationsData').resolves(data); - const response = await service.getSurveyLocationData(1); + const response = await service.getSurveyLocationsData(1); expect(repoStub).to.be.calledOnce; expect(response).to.eql(data); diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index c780ac01a9..b7b7c3d5f3 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -1,8 +1,8 @@ import { Feature } from 'geojson'; import { MESSAGE_CLASS_NAME, SUBMISSION_MESSAGE_TYPE, SUBMISSION_STATUS_TYPE } from '../constants/status'; import { IDBConnection } from '../database/db'; -import { PostProprietorData, PostSurveyObject } from '../models/survey-create'; -import { PutPartnershipsData, PutSurveyObject } from '../models/survey-update'; +import { PostLocationData, PostProprietorData, PostSurveyObject } from '../models/survey-create'; +import { PutPartnershipsData, PutSurveyLocationData, PutSurveyObject } from '../models/survey-update'; import { GetAncillarySpeciesData, GetAttachmentsData, @@ -11,7 +11,6 @@ import { GetReportAttachmentsData, GetSurveyData, GetSurveyFundingSourceData, - GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData, ISurveyPartnerships, @@ -21,6 +20,7 @@ import { import { AttachmentRepository } from '../repositories/attachment-repository'; import { PublishStatus } from '../repositories/history-publish-repository'; import { PostSurveyBlock, SurveyBlockRecord } from '../repositories/survey-block-repository'; +import { SurveyLocationRecord } from '../repositories/survey-location-repository'; import { IGetLatestSurveyOccurrenceSubmission, IObservationSubmissionInsertDetails, @@ -38,6 +38,7 @@ import { PlatformService } from './platform-service'; import { RegionService } from './region-service'; import { SiteSelectionStrategyService } from './site-selection-strategy-service'; import { SurveyBlockService } from './survey-block-service'; +import { SurveyLocationService } from './survey-location-service'; import { SurveyParticipationService } from './survey-participation-service'; import { TaxonomyService } from './taxonomy-service'; @@ -98,9 +99,9 @@ export class SurveyService extends DBService { partnerships: await this.getSurveyPartnershipsData(surveyId), purpose_and_methodology: await this.getSurveyPurposeAndMethodology(surveyId), proprietor: await this.getSurveyProprietorDataForView(surveyId), - location: await this.getSurveyLocationData(surveyId), - site_selection: await this.siteSelectionStrategyService.getSiteSelectionDataBySurveyId(surveyId), + locations: await this.getSurveyLocationsData(surveyId), participants: await this.surveyParticipationService.getSurveyParticipants(surveyId), + site_selection: await this.siteSelectionStrategyService.getSiteSelectionDataBySurveyId(surveyId), blocks: await this.getSurveyBlocksForSurveyId(surveyId) }; } @@ -226,11 +227,12 @@ export class SurveyService extends DBService { * Get Survey location for a given survey ID * * @param {number} surveyID - * @returns {*} {Promise} + * @returns {*} {Promise} * @memberof SurveyService */ - async getSurveyLocationData(surveyId: number): Promise { - return this.surveyRepository.getSurveyLocationData(surveyId); + async getSurveyLocationsData(surveyId: number): Promise { + const service = new SurveyLocationService(this.connection); + return service.getSurveyLocationsData(surveyId); } /** @@ -452,9 +454,8 @@ export class SurveyService extends DBService { ) ); - // Handle regions associated to a survey - if (postSurveyData.location) { - promises.push(this.insertRegion(surveyId, postSurveyData.location.geometry)); + if (postSurveyData.locations) { + promises.push(Promise.all(postSurveyData.locations.map((item) => this.insertSurveyLocations(surveyId, item)))); } // Handle site selection strategies @@ -485,6 +486,19 @@ export class SurveyService extends DBService { return surveyId; } + /** + * Inserts location data. + * + * @param {number} surveyId + * @param {PostLocationData} data + * @return {*} {Promise} + * @memberof SurveyService + */ + async insertSurveyLocations(surveyId: number, data: PostLocationData): Promise { + const service = new SurveyLocationService(this.connection); + return service.insertSurveyLocation(surveyId, data); + } + /** * Insert, updates and deletes Survey Blocks for a given survey id * @@ -498,6 +512,14 @@ export class SurveyService extends DBService { return service.upsertSurveyBlocks(surveyId, blocks); } + /** + * Insert region data. + * + * @param {number} projectId + * @param {Feature[]} features + * @return {*} {Promise} + * @memberof SurveyService + */ async insertRegion(projectId: number, features: Feature[]): Promise { const regionService = new RegionService(this.connection); return regionService.addRegionsToSurveyFromFeatures(projectId, features); @@ -661,8 +683,7 @@ export class SurveyService extends DBService { */ async updateSurvey(surveyId: number, putSurveyData: PutSurveyObject): Promise { const promises: Promise[] = []; - - if (putSurveyData?.survey_details || putSurveyData?.purpose_and_methodology || putSurveyData?.location) { + if (putSurveyData?.survey_details || putSurveyData?.purpose_and_methodology) { promises.push(this.updateSurveyDetailsData(surveyId, putSurveyData)); } @@ -689,13 +710,12 @@ export class SurveyService extends DBService { if (putSurveyData?.funding_sources) { promises.push(this.upsertSurveyFundingSourceData(surveyId, putSurveyData)); } - if (putSurveyData?.proprietor) { promises.push(this.updateSurveyProprietorData(surveyId, putSurveyData)); } - if (putSurveyData?.location) { - promises.push(this.insertRegion(surveyId, putSurveyData?.location.geometry)); + if (putSurveyData?.locations) { + promises.push(Promise.all(putSurveyData.locations.map((item) => this.updateSurveyLocation(item)))); } if (putSurveyData?.participants.length) { @@ -730,6 +750,11 @@ export class SurveyService extends DBService { await Promise.all(promises); } + async updateSurveyLocation(data: PutSurveyLocationData): Promise { + const surveyLocationService = new SurveyLocationService(this.connection); + return surveyLocationService.updateSurveyLocation(data); + } + /** * Updates Survey details * diff --git a/api/src/zod-schema/json.ts b/api/src/zod-schema/json.ts index 103dd25af1..4b101baa54 100644 --- a/api/src/zod-schema/json.ts +++ b/api/src/zod-schema/json.ts @@ -1,5 +1,6 @@ import * as z from 'zod'; +// Defines a Zod Schema for a valid JSON value const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; type Json = Literal | { [key: string]: Json } | Json[]; diff --git a/app/package-lock.json b/app/package-lock.json index cce60258f3..e9f6dad851 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -4478,7 +4478,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", @@ -4580,12 +4580,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", @@ -4699,7 +4699,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", @@ -4716,12 +4716,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", @@ -4757,7 +4757,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", @@ -5061,7 +5061,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" } @@ -5093,7 +5093,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" } @@ -5259,7 +5259,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", @@ -5270,7 +5270,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" @@ -5302,7 +5302,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", @@ -5442,7 +5442,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", @@ -5461,7 +5461,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", @@ -5563,7 +5563,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", @@ -5591,7 +5591,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", @@ -5619,7 +5619,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", @@ -6036,7 +6036,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" } @@ -6050,7 +6050,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" } @@ -6077,7 +6077,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", @@ -6142,17 +6142,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", @@ -6163,7 +6163,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", @@ -6378,7 +6378,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" @@ -6387,7 +6387,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", @@ -6423,7 +6423,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", @@ -6587,12 +6587,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", @@ -7278,7 +7278,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", @@ -7411,7 +7411,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", @@ -7594,7 +7594,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" @@ -7644,7 +7644,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", @@ -7835,7 +7835,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-extra": { "version": "10.1.0", @@ -7857,7 +7857,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", @@ -7902,7 +7902,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", @@ -7930,7 +7930,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" } @@ -7966,7 +7966,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", @@ -7987,7 +7987,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" } @@ -8141,7 +8141,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", @@ -8169,7 +8169,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" } @@ -8183,7 +8183,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", @@ -8214,7 +8214,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", @@ -8427,7 +8427,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", @@ -8437,7 +8437,7 @@ "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, "https-proxy-agent": { @@ -8504,7 +8504,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", @@ -8553,7 +8553,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" @@ -8609,7 +8609,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", @@ -8687,7 +8687,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" } @@ -8864,12 +8864,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", @@ -8908,17 +8908,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", @@ -11457,7 +11457,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", @@ -11554,7 +11554,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", @@ -11751,7 +11751,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", @@ -11849,7 +11849,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=" } } }, @@ -11904,7 +11904,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" @@ -11963,7 +11963,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", @@ -11974,7 +11974,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", @@ -11993,7 +11993,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", @@ -12010,7 +12010,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", @@ -12027,7 +12027,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", @@ -12301,12 +12301,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", @@ -12318,14 +12318,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" } @@ -12398,7 +12398,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", @@ -12414,7 +12414,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", @@ -12519,7 +12519,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" } @@ -12533,7 +12533,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" } @@ -12575,12 +12575,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", @@ -12689,7 +12689,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" } @@ -12697,7 +12697,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", @@ -12713,7 +12713,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", @@ -12723,7 +12723,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", @@ -12739,17 +12739,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" } @@ -13756,7 +13756,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", @@ -15214,7 +15214,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" @@ -15439,7 +15439,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" } @@ -15486,7 +15486,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", @@ -15788,7 +15788,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" @@ -15797,7 +15797,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" } @@ -15945,7 +15945,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", @@ -15988,7 +15988,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=" } } }, @@ -16040,7 +16040,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", @@ -16216,7 +16216,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", @@ -16271,7 +16271,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", @@ -16356,7 +16356,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" } @@ -16364,7 +16364,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" } @@ -16793,7 +16793,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", @@ -16812,7 +16812,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", @@ -16835,7 +16835,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", @@ -16911,7 +16911,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" } @@ -16919,7 +16919,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", @@ -17070,7 +17070,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", @@ -17127,7 +17127,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", @@ -17150,7 +17150,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", @@ -17185,12 +17185,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", @@ -17894,7 +17894,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", @@ -17919,7 +17919,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", @@ -17940,7 +17940,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": { @@ -18003,7 +18003,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/fields/MultiAutocompleteField.tsx b/app/src/components/fields/MultiAutocompleteField.tsx index c4bb952c60..a90265e062 100644 --- a/app/src/components/fields/MultiAutocompleteField.tsx +++ b/app/src/components/fields/MultiAutocompleteField.tsx @@ -1,11 +1,16 @@ import CheckBox from '@mui/icons-material/CheckBox'; import CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank'; -import { Chip } from '@mui/material'; -import Autocomplete, { AutocompleteInputChangeReason, createFilterOptions } from '@mui/material/Autocomplete'; +import Autocomplete, { + AutocompleteChangeReason, + AutocompleteInputChangeReason, + createFilterOptions +} from '@mui/material/Autocomplete'; import Box from '@mui/material/Box'; import Checkbox from '@mui/material/Checkbox'; +import Chip from '@mui/material/Chip'; import TextField from '@mui/material/TextField'; import { useFormikContext } from 'formik'; +import get from 'lodash-es/get'; import { useEffect, useState } from 'react'; export interface IMultiAutocompleteFieldOption { @@ -17,9 +22,15 @@ export interface IMultiAutocompleteField { id: string; label: string; options: IMultiAutocompleteFieldOption[]; + selectedOptions?: IMultiAutocompleteFieldOption[]; required?: boolean; filterLimit?: number; chipVisible?: boolean; + onChange?: ( + _event: React.ChangeEvent, + selectedOptions: IMultiAutocompleteFieldOption[], + reason: AutocompleteChangeReason + ) => void; handleSearchResults?: (input: string) => Promise; } @@ -44,8 +55,7 @@ export const sortAutocompleteOptions = ( }; const MultiAutocompleteField: React.FC = (props) => { - const { getFieldMeta, setFieldValue } = useFormikContext(); - const { value, touched, error } = getFieldMeta(props.id); + const { values, touched, errors, setFieldValue } = useFormikContext(); const [inputValue, setInputValue] = useState(''); const [options, setOptions] = useState(props.options || []); // store options if provided @@ -65,12 +75,12 @@ const MultiAutocompleteField: React.FC = (props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [inputValue]); - const handleOnChange = (_event: React.ChangeEvent, selectedOptions: IMultiAutocompleteFieldOption[]) => { + const defaultHandleOnChange = (_event: React.ChangeEvent, selectedOptions: IMultiAutocompleteFieldOption[]) => { setOptions(sortAutocompleteOptions(selectedOptions, options)); setSelectedOptions(selectedOptions); setFieldValue( props.id, - selectedOptions.map((item) => item) + selectedOptions.map((item) => item.value) ); }; @@ -93,45 +103,45 @@ const MultiAutocompleteField: React.FC = (props) => { } }; - const defaultChipDisplay = (option: any, renderProps: any, checkedStatus: any) => { - return ( - - } - checkedIcon={} - style={{ marginRight: 8 }} - checked={checkedStatus} - disabled={(options && options?.indexOf(option) !== -1) || false} - value={option.value} - color="default" - /> - {option.label} - - ); + const getExistingValue = (existingValues?: (number | string)[]): IMultiAutocompleteFieldOption[] => { + if (existingValues) { + return options.filter((option) => existingValues.includes(option.value)); + } + return []; }; - const existingValues: IMultiAutocompleteFieldOption[] = - value && value.length > 0 ? options.filter((option) => value.includes(option)) : []; return ( option.label} isOptionEqualToValue={handleGetOptionSelected} - filterOptions={createFilterOptions({ limit: props.filterLimit })} disableCloseOnSelect - onChange={handleOnChange} + disableListWrap inputValue={inputValue} onInputChange={handleOnInputChange} - renderTags={(tagValue, getTagProps) => { - if (props.chipVisible) { - return tagValue.map((option, index) => ); - } + onChange={props.onChange ? props.onChange : defaultHandleOnChange} + filterOptions={createFilterOptions({ limit: props.filterLimit })} + renderOption={(renderProps, renderOption, { selected }) => { + return ( + + } + checkedIcon={} + checked={selected} + disabled={props.options.includes(renderOption) || false} + value={renderOption.value} + color="default" + /> + {renderOption.label} + + ); }} - renderOption={(_renderProps, option, { selected }) => defaultChipDisplay(option, _renderProps, selected)} renderInput={(params) => ( { @@ -140,18 +150,23 @@ const MultiAutocompleteField: React.FC = (props) => { } }} {...params} + name={props.id} required={props.required} label={props.label} variant="outlined" fullWidth - error={touched && Boolean(error)} - helperText={touched && error} - placeholder={'Begin typing to filter results...'} - InputLabelProps={{ - shrink: true - }} + placeholder="Type to start searching" + error={get(touched, props.id) && Boolean(get(errors, props.id))} + helperText={get(touched, props.id) && get(errors, props.id)} /> )} + renderTags={(tagValue, getTagProps) => { + if (props.chipVisible === false) { + return; + } + + return tagValue.map((option, index) => ); + }} /> ); }; diff --git a/app/src/components/fields/MultiAutocompleteFieldVariableSize.tsx b/app/src/components/fields/MultiAutocompleteFieldVariableSize.tsx index 51c9b5810e..e8cb052405 100644 --- a/app/src/components/fields/MultiAutocompleteFieldVariableSize.tsx +++ b/app/src/components/fields/MultiAutocompleteFieldVariableSize.tsx @@ -248,7 +248,7 @@ const MultiAutocompleteFieldVariableSize: React.FC = (p return ( >} @@ -272,7 +272,7 @@ const MultiAutocompleteFieldVariableSize: React.FC = (p checkedIcon={} checked={selected} // Always seem to be disabled - disabled={(props.options && props.options?.indexOf(renderOption) !== -1) || false} + disabled={props.options?.includes(renderOption) || false} value={renderOption.value} color="default" /> @@ -288,6 +288,7 @@ const MultiAutocompleteFieldVariableSize: React.FC = (p label={props.label} variant="outlined" fullWidth + placeholder="Type to start searching" error={get(touched, props.id) && Boolean(get(errors, props.id))} helperText={get(touched, props.id) && get(errors, props.id)} /> diff --git a/app/src/components/fields/SingleDateField.tsx b/app/src/components/fields/SingleDateField.tsx index 27b15c2e83..43313cd1d7 100644 --- a/app/src/components/fields/SingleDateField.tsx +++ b/app/src/components/fields/SingleDateField.tsx @@ -1,3 +1,5 @@ +import { mdiCalendar } from '@mdi/js'; +import Icon from '@mdi/react'; import { TextFieldProps } from '@mui/material/TextField'; import { DatePicker } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; @@ -16,6 +18,10 @@ interface IDateProps { other?: TextFieldProps; } +const CalendarIcon = () => { + return ; +}; + /** * Single date field * @@ -40,6 +46,9 @@ const SingleDateField: React.FC = (props) => { return ( = (props) => { maxDate={moment(DATE_LIMIT.max)} value={formattedDateValue} onChange={(value) => { + if (!value || String(value.creationData().input) === 'Invalid Date') { + // The creation input value will be 'Invalid Date' when the date field is cleared (empty), and will + // contain an actual date string value if the field is not empty but is invalid. + setFieldValue(name, null); + return; + } + setFieldValue(name, moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> diff --git a/app/src/components/fields/StartEndDateFields.tsx b/app/src/components/fields/StartEndDateFields.tsx index cb0983cc36..67a1d7b26a 100644 --- a/app/src/components/fields/StartEndDateFields.tsx +++ b/app/src/components/fields/StartEndDateFields.tsx @@ -19,11 +19,11 @@ interface IStartEndDateFieldsProps { endDateHelperText?: string; } -const CalendarStartIcon: React.FC = () => { +const CalendarStartIcon = () => { return ; }; -const CalendarEndIcon: React.FC = () => { +const CalendarEndIcon = () => { return ; }; @@ -88,6 +88,13 @@ const StartEndDateFields: React.FC = (props) => { maxDate={moment(DATE_LIMIT.max)} value={formattedStartDateValue} onChange={(value) => { + if (!value || String(value.creationData().input) === 'Invalid Date') { + // The creation input value will be 'Invalid Date' when the date field is cleared (empty), and will + // contain an actual date string value if the field is not empty but is invalid. + setFieldValue(startName, null); + return; + } + setFieldValue(startName, moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> @@ -119,7 +126,14 @@ const StartEndDateFields: React.FC = (props) => { minDate={moment(DATE_LIMIT.min)} maxDate={moment(DATE_LIMIT.max)} value={formattedEndDateValue} - onChange={(value) => { + onChange={(value: moment.Moment | null) => { + if (!value || String(value.creationData().input) === 'Invalid Date') { + // The creation input value will be 'Invalid Date' when the date field is cleared (empty), and will + // contain an actual date string value if the field is not empty but is invalid. + setFieldValue(endName, null); + return; + } + setFieldValue(endName, moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> diff --git a/app/src/features/projects/components/ProjectDetailsForm.tsx b/app/src/features/projects/components/ProjectDetailsForm.tsx index 64a570c4c6..fb2878008f 100644 --- a/app/src/features/projects/components/ProjectDetailsForm.tsx +++ b/app/src/features/projects/components/ProjectDetailsForm.tsx @@ -80,7 +80,7 @@ const ProjectDetailsForm: React.FC = (props) => { /> - + , []>() + }, + project: { + createProject: jest.fn, []>() + }, + user: { + searchSystemUser: jest.fn, []>() } }; @@ -70,6 +78,8 @@ describe('CreateProjectPage', () => { mockUseApi.draft.getDraft.mockClear(); mockUseApi.spatial.getRegions.mockClear(); mockUseApi.codes.getAllCodeSets.mockClear(); + mockUseApi.project.createProject.mockClear(); + mockUseApi.user.searchSystemUser.mockClear(); mockUseApi.spatial.getRegions.mockResolvedValue({ regions: [] diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index b3b386e59c..990cf1000f 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -43,7 +43,7 @@ import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from './components/PurposeAndMethodologyForm'; import SamplingMethodsForm from './components/SamplingMethodsForm'; -import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from './components/StudyAreaForm'; +import StudyAreaForm, { SurveyLocationInitialValues, SurveyLocationYupSchema } from './components/StudyAreaForm'; import { SurveyBlockInitialValues } from './components/SurveyBlockSection'; import SurveyFundingSourceForm, { SurveyFundingSourceFormInitialValues, @@ -134,11 +134,11 @@ const CreateSurveyPage = () => { const [surveyInitialValues] = useState({ ...GeneralInformationInitialValues, ...PurposeAndMethodologyInitialValues, - ...StudyAreaInitialValues, ...SurveyFundingSourceFormInitialValues, ...SurveyPartnershipsFormInitialValues, ...ProprietaryDataInitialValues, ...AgreementsInitialValues, + ...SurveyLocationInitialValues, ...SurveySiteSelectionInitialValues, ...SurveyUserJobFormInitialValues, ...SurveyBlockInitialValues @@ -179,12 +179,12 @@ const CreateSurveyPage = () => { `Survey end date cannot be after ${getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, DATE_LIMIT.max)}` ) }) - .concat(StudyAreaYupSchema) .concat(PurposeAndMethodologyYupSchema) .concat(ProprietaryDataYupSchema) .concat(SurveyFundingSourceFormYupSchema) .concat(AgreementsYupSchema) .concat(SurveyUserJobYupSchema) + .concat(SurveyLocationYupSchema) .concat(SurveySiteSelectionYupSchema) .concat(SurveyPartnershipsFormYupSchema); @@ -266,7 +266,6 @@ const CreateSurveyPage = () => { if (!codes || !projectData) { return ; } - return ( <> @@ -430,7 +429,9 @@ const CreateSurveyPage = () => { type="submit" variant="contained" color="primary" - onClick={() => formikRef.current?.submitForm()} + onClick={() => { + formikRef.current?.submitForm(); + }} className={classes.actionButton}> Save and Exit diff --git a/app/src/features/surveys/components/StudyAreaForm.test.tsx b/app/src/features/surveys/components/StudyAreaForm.test.tsx index 9f56da32f4..ad0079d7e2 100644 --- a/app/src/features/surveys/components/StudyAreaForm.test.tsx +++ b/app/src/features/surveys/components/StudyAreaForm.test.tsx @@ -1,9 +1,9 @@ import { cleanup } from '@testing-library/react-hooks'; import MapBoundary from 'components/boundary/MapBoundary'; import StudyAreaForm, { - IStudyAreaForm, - StudyAreaInitialValues, - StudyAreaYupSchema + ISurveyLocationForm, + SurveyLocationInitialValues, + SurveyLocationYupSchema } from 'features/surveys/components/StudyAreaForm'; import { Formik } from 'formik'; import { render, waitFor } from 'test-helpers/test-utils'; @@ -24,8 +24,8 @@ describe('Study Area Form', () => { it('renders correctly with default values', async () => { const { getByLabelText, getByTestId } = render( @@ -37,43 +37,46 @@ describe('Study Area Form', () => { // Assert MapBoundary was rendered with the right propsF expect(MapBoundary).toHaveBeenCalledWith( { - name: 'location.geometry', + name: 'locations[0].geojson', title: 'Study Area Boundary', mapId: 'study_area_form_map', bounds: undefined, - formikProps: expect.objectContaining({ values: StudyAreaInitialValues }) + formikProps: expect.objectContaining({ values: SurveyLocationInitialValues }) }, expect.anything() ); // Assert survey area name field is visible and populated correctly expect(getByLabelText('Survey Area Name', { exact: false })).toBeVisible(); - expect(getByTestId('location.survey_area_name')).toHaveValue(''); + expect(getByTestId('locations[0].name')).toHaveValue(''); }); }); it('renders correctly with non default values', async () => { - const existingFormValues: IStudyAreaForm = { - location: { - survey_area_name: 'a study area name', - geometry: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [125.6, 10.1] - }, - properties: { - name: 'Dinagat Islands' + const existingFormValues: ISurveyLocationForm = { + locations: [ + { + name: 'a study area name', + description: 'a study area description', + geojson: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [125.6, 10.1] + }, + properties: { + name: 'Dinagat Islands' + } } - } - ] - } + ] + } + ] }; const { getByLabelText, getByTestId } = render( @@ -85,7 +88,7 @@ describe('Study Area Form', () => { // Assert MapBoundary was rendered with the right propsF expect(MapBoundary).toHaveBeenCalledWith( { - name: 'location.geometry', + name: 'locations[0].geojson', title: 'Study Area Boundary', mapId: 'study_area_form_map', bounds: undefined, @@ -95,7 +98,7 @@ describe('Study Area Form', () => { ); // Assert survey area name field is visible and populated correctly expect(getByLabelText('Survey Area Name', { exact: false })).toBeVisible(); - expect(getByTestId('location.survey_area_name')).toHaveValue('a study area name'); + expect(getByTestId('locations[0].name')).toHaveValue('a study area name'); }); }); }); diff --git a/app/src/features/surveys/components/StudyAreaForm.tsx b/app/src/features/surveys/components/StudyAreaForm.tsx index 84f0c26a34..53bab39238 100644 --- a/app/src/features/surveys/components/StudyAreaForm.tsx +++ b/app/src/features/surveys/components/StudyAreaForm.tsx @@ -5,25 +5,38 @@ import { useFormikContext } from 'formik'; import { Feature } from 'geojson'; import yup from 'utils/YupSchema'; -export interface IStudyAreaForm { - location: { - survey_area_name: string; - geometry: Feature[]; - }; +export interface ISurveyLocationForm { + locations: { + survey_location_id?: number; + name: string; + description: string; + geojson: Feature[]; + revision_count?: number; + }[]; } -export const StudyAreaInitialValues: IStudyAreaForm = { - location: { - survey_area_name: '', - geometry: [] - } +export const SurveyLocationInitialValues: ISurveyLocationForm = { + locations: [ + { + survey_location_id: null as unknown as number, + name: '', + // TODO description is temporarily hardcoded until the new UI to populate this field is implemented in + // https://apps.nrs.gov.bc.ca/int/jira/browse/SIMSBIOHUB-219 + description: 'Insert description here', + geojson: [], + revision_count: 0 + } + ] }; -export const StudyAreaYupSchema = yup.object().shape({ - location: yup.object().shape({ - survey_area_name: yup.string().required('Survey Area Name is Required'), - geometry: yup.array().min(1, 'A survey study area is required').required('A survey study area is required') - }) +export const SurveyLocationYupSchema = yup.object({ + locations: yup.array( + yup.object({ + name: yup.string().max(100, 'Name cannot exceed 100 characters').required('Name is Required'), + description: yup.string().max(250, 'Description cannot exceed 250 characters'), + geojson: yup.array().min(1, 'A geometry is required').required('A geometry is required') + }) + ) }); /** @@ -32,15 +45,14 @@ export const StudyAreaYupSchema = yup.object().shape({ * @return {*} */ const StudyAreaForm = () => { - const formikProps = useFormikContext(); + const formikProps = useFormikContext(); const { handleSubmit } = formikProps; - return (
{ /> { - const entries = (stratums || []).map((stratum) => new String(stratum.name).trim()); + const entries = (stratums || []).map((stratum) => String(stratum.name).trim()); return new Set(entries).size === stratums?.length; }) - */ }) }); @@ -79,31 +78,34 @@ const SurveySiteSelectionForm = (props: ISurveySiteSelectionFormProps) => { const siteStrategies = codesContext.codesDataLoader.data.site_selection_strategies.map((code) => { return { label: code.name, value: code.name }; }); + const selectedSiteStrategies = siteStrategies.filter((item) => values.site_selection.strategies.includes(item.value)); const handleConfirmDeleteAllStratums = () => { - // Delete all Stratums and hide the Stratums form + // Delete all Stratums setFieldValue('site_selection.stratums', []); + // Remove 'Stratified' from the list of selected strategies + setFieldValue( + 'site_selection.strategies', + values.site_selection.strategies.filter((item) => item !== 'Stratified') + ); + // Hide Stratums form props.onChangeStratumEntryVisibility(false); + // Close dialogue setShowStratumDeleteConfirmModal(false); }; const handleCancelDeleteAllStratums = () => { + // Close dialogue and do nothing setShowStratumDeleteConfirmModal(false); - setFieldValue('site_selection.strategies', [...values.site_selection.strategies, 'Stratified']); }; useEffect(() => { if (values.site_selection.strategies.includes('Stratified')) { props.onChangeStratumEntryVisibility(true); - } else if (values.site_selection.stratums.length > 0) { - // Prompt to confirm removing all stratums - setShowStratumDeleteConfirmModal(true); } else { props.onChangeStratumEntryVisibility(false); } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [values.site_selection.strategies]); + }, [props, values.site_selection.strategies]); return ( <> @@ -123,11 +125,31 @@ const SurveySiteSelectionForm = (props: ISurveySiteSelectionFormProps) => { onClose={handleCancelDeleteAllStratums} onYes={handleConfirmDeleteAllStratums} /> - { + // If the user clicks to remove the 'Stratified' option and there are Stratums already defined, then show + // a warning dialogue asking the user if they are sure they want to remove the option and delete the Stratums + if ( + reason === 'removeOption' && + values.site_selection.strategies.includes('Stratified') && + !selectedOptions.map((item) => item.value).includes('Stratified') && + values.site_selection.stratums.length + ) { + setShowStratumDeleteConfirmModal(true); + return; + } + + // Update selected options + setFieldValue( + 'site_selection.strategies', + selectedOptions.map((item) => item.value) + ); + }} /> ); diff --git a/app/src/features/surveys/components/SurveyStratumForm.tsx b/app/src/features/surveys/components/SurveyStratumForm.tsx index ae2192d9fd..f674928491 100644 --- a/app/src/features/surveys/components/SurveyStratumForm.tsx +++ b/app/src/features/surveys/components/SurveyStratumForm.tsx @@ -1,14 +1,19 @@ import { mdiDotsVertical, mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; -import { ListItemIcon, Menu, MenuItem, MenuProps } from '@mui/material'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; import Collapse from '@mui/material/Collapse'; import { grey } from '@mui/material/colors'; import IconButton from '@mui/material/IconButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Menu, { MenuProps } from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; import { FormikProps, useFormikContext } from 'formik'; import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface'; +import get from 'lodash-es/get'; import { useState } from 'react'; import { TransitionGroup } from 'react-transition-group'; import yup from 'utils/YupSchema'; @@ -51,7 +56,7 @@ const SurveyStratumForm = () => { const [anchorEl, setAnchorEl] = useState(null); const formikProps = useFormikContext(); - const { values, handleSubmit, setFieldValue } = formikProps; + const { values, errors, handleSubmit, setFieldValue } = formikProps; const handleSave = (formikProps: FormikProps | null) => { if (!formikProps) { @@ -137,6 +142,14 @@ const SurveyStratumForm = () => { + {get(errors, 'site_selection.stratums') && ( + // Show array level error, if any + + + {get(errors, 'site_selection.stratums') as string} + + + )} {values.site_selection.stratums.map((stratum: IStratum, index: number) => { const key = `${stratum.name}-${index}`; diff --git a/app/src/features/surveys/edit/EditSurveyForm.tsx b/app/src/features/surveys/edit/EditSurveyForm.tsx index 160418f9be..16b2049cce 100644 --- a/app/src/features/surveys/edit/EditSurveyForm.tsx +++ b/app/src/features/surveys/edit/EditSurveyForm.tsx @@ -28,7 +28,7 @@ import GeneralInformationForm, { import ProprietaryDataForm, { ProprietaryDataYupSchema } from '../components/ProprietaryDataForm'; import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from '../components/PurposeAndMethodologyForm'; import SamplingMethodsForm from '../components/SamplingMethodsForm'; -import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from '../components/StudyAreaForm'; +import StudyAreaForm, { SurveyLocationInitialValues, SurveyLocationYupSchema } from '../components/StudyAreaForm'; import { SurveyBlockInitialValues } from '../components/SurveyBlockSection'; import SurveyFundingSourceForm, { SurveyFundingSourceFormInitialValues, @@ -80,7 +80,7 @@ const EditSurveyForm: React.FC = (props) => { vantage_code_ids: [] } }, - ...StudyAreaInitialValues, + ...SurveyLocationInitialValues, ...SurveyFundingSourceFormInitialValues, ...SurveyPartnershipsFormInitialValues, ...SurveySiteSelectionInitialValues, @@ -140,7 +140,7 @@ const EditSurveyForm: React.FC = (props) => { ) .nullable() }) - .concat(StudyAreaYupSchema) + .concat(SurveyLocationYupSchema) .concat(PurposeAndMethodologyYupSchema) .concat(ProprietaryDataYupSchema) .concat(SurveyFundingSourceFormYupSchema) diff --git a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx index 7e7587dc4f..4187e864cb 100644 --- a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx +++ b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx @@ -4,7 +4,6 @@ import { GetRegionsResponse } from 'hooks/api/useSpatialApi'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { DataLoader } from 'hooks/useDataLoader'; import { IGetSurveyForViewResponse } from 'interfaces/useSurveyApi.interface'; -import { geoJsonFeature } from 'test-helpers/spatial-helpers'; import { getSurveyForViewResponse, surveyObject, surveySupplementaryData } from 'test-helpers/survey-helpers'; import { cleanup, fireEvent, render, waitFor } from 'test-helpers/test-utils'; import SurveyStudyArea from './SurveyStudyArea'; @@ -72,7 +71,7 @@ describe('SurveyStudyArea', () => { ...getSurveyForViewResponse, surveyData: { ...getSurveyForViewResponse.surveyData, - survey_details: { ...getSurveyForViewResponse.surveyData.survey_details, geometry: [] } + survey_details: { ...getSurveyForViewResponse.surveyData.survey_details, geojson: [] } } } } as DataLoader; @@ -96,7 +95,7 @@ describe('SurveyStudyArea', () => { await waitFor(() => { expect(container).toBeVisible(); - expect(queryByTestId('survey_map_center_button')).not.toBeInTheDocument(); + expect(queryByTestId('survey_map_center_button')).toBeInTheDocument(); }); }); @@ -187,31 +186,35 @@ describe('SurveyStudyArea', () => { await waitFor(() => { expect(mockUseApi.survey.updateSurvey).toBeCalledWith(1, getSurveyForViewResponse.surveyData.survey_details.id, { - location: { - geometry: [ - { - geometry: { - coordinates: [ - [ - [-128, 55], - [-128, 55.5], - [-128, 56], - [-126, 58], - [-128, 55] - ] - ], - type: 'Polygon' - }, - id: 'myGeo', - properties: { - name: 'Biohub Islands' - }, - type: 'Feature' - } - ], - revision_count: 0, - survey_area_name: 'study area' - } + locations: [ + { + survey_location_id: 1, + geojson: [ + { + geometry: { + coordinates: [ + [ + [-128, 55], + [-128, 55.5], + [-128, 56], + [-126, 58], + [-128, 55] + ] + ], + type: 'Polygon' + }, + id: 'myGeo', + properties: { + name: 'Biohub Islands' + }, + type: 'Feature' + } + ], + revision_count: 0, + name: 'study area', + description: 'study area description' + } + ] }); }); }); @@ -233,8 +236,6 @@ describe('SurveyStudyArea', () => { end_date: '2021-01-25', biologist_first_name: 'firstttt', biologist_last_name: 'lastttt', - survey_area_name: 'study area is this', - geometry: [geoJsonFeature], survey_types: [1], revision_count: 0 } diff --git a/app/src/features/surveys/view/components/SurveyStudyArea.tsx b/app/src/features/surveys/view/components/SurveyStudyArea.tsx index 91eaa63c7c..1dc2c3d129 100644 --- a/app/src/features/surveys/view/components/SurveyStudyArea.tsx +++ b/app/src/features/surveys/view/components/SurveyStudyArea.tsx @@ -22,9 +22,9 @@ import { EditSurveyStudyAreaI18N } from 'constants/i18n'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; import { SurveyContext } from 'contexts/surveyContext'; import StudyAreaForm, { - IStudyAreaForm, - StudyAreaInitialValues, - StudyAreaYupSchema + ISurveyLocationForm, + SurveyLocationInitialValues, + SurveyLocationYupSchema } from 'features/surveys/components/StudyAreaForm'; import { Feature } from 'geojson'; import { APIError } from 'hooks/api/useAxios'; @@ -79,11 +79,12 @@ const SurveyStudyArea = () => { const [markerLayers, setMarkerLayers] = useState([]); const [staticLayers, setStaticLayers] = useState([]); - const survey_details = surveyContext.surveyDataLoader.data?.surveyData?.survey_details; - const surveyGeometry = useMemo(() => survey_details?.geometry || [], [survey_details]); + const surveyLocations = surveyContext.surveyDataLoader.data?.surveyData?.locations; + const surveyLocation = surveyLocations[0] || null; + const surveyGeometry = useMemo(() => surveyLocation?.geojson || [], [surveyLocation]); const [openEditDialog, setOpenEditDialog] = useState(false); - const [studyAreaFormData, setStudyAreaFormData] = useState(StudyAreaInitialValues); + const [studyAreaFormData, setStudyAreaFormData] = useState(SurveyLocationInitialValues); const [bounds, setBounds] = useState(undefined); const [showFullScreenViewMapDialog, setShowFullScreenViewMapDialog] = useState(false); @@ -155,32 +156,41 @@ const SurveyStudyArea = () => { }; const handleDialogEditOpen = () => { - if (!survey_details) { + if (!surveyLocation) { return; } setStudyAreaFormData({ - location: { - survey_area_name: survey_details.survey_area_name, - geometry: survey_details.geometry - } + locations: [ + { + survey_location_id: surveyLocation.survey_location_id, + name: surveyLocation.name, + description: surveyLocation.description, + geojson: surveyLocation.geojson, + revision_count: surveyLocation.revision_count + } + ] }); setOpenEditDialog(true); }; - const handleDialogEditSave = async (values: IStudyAreaForm) => { - if (!survey_details) { + const handleDialogEditSave = async (values: ISurveyLocationForm) => { + if (!surveyLocation) { return; } try { const surveyData = { - location: { - survey_area_name: values.location.survey_area_name, - geometry: values.location.geometry, - revision_count: survey_details.revision_count - } + locations: values.locations.map((item) => { + return { + survey_location_id: item.survey_location_id, + name: item.name, + description: item.description, + geojson: item.geojson, + revision_count: surveyLocation.revision_count + }; + }) }; await biohubApi.survey.updateSurvey(surveyContext.projectId, surveyContext.surveyId, surveyData); @@ -211,7 +221,7 @@ const SurveyStudyArea = () => { component={{ element: , initialValues: studyAreaFormData, - validationSchema: StudyAreaYupSchema + validationSchema: SurveyLocationYupSchema }} onCancel={() => setOpenEditDialog(false)} onSave={handleDialogEditSave} @@ -231,7 +241,7 @@ const SurveyStudyArea = () => { staticLayers={staticLayers} /> } - description={survey_details?.survey_area_name} + description={surveyLocation?.name} layers={} backButtonTitle={'Back To Survey'} mapTitle={'Study Area'} @@ -283,7 +293,7 @@ const SurveyStudyArea = () => { Study Area Name - {survey_details?.survey_area_name} + {surveyLocation?.name} diff --git a/app/src/interfaces/useSurveyApi.interface.ts b/app/src/interfaces/useSurveyApi.interface.ts index f339b444f8..69d1bb1896 100644 --- a/app/src/interfaces/useSurveyApi.interface.ts +++ b/app/src/interfaces/useSurveyApi.interface.ts @@ -3,7 +3,7 @@ import { IAgreementsForm } from 'features/surveys/components/AgreementsForm'; import { IGeneralInformationForm } from 'features/surveys/components/GeneralInformationForm'; import { IProprietaryDataForm } from 'features/surveys/components/ProprietaryDataForm'; import { IPurposeAndMethodologyForm } from 'features/surveys/components/PurposeAndMethodologyForm'; -import { IStudyAreaForm } from 'features/surveys/components/StudyAreaForm'; +import { ISurveyLocationForm } from 'features/surveys/components/StudyAreaForm'; import { ISurveyFundingSource, ISurveyFundingSourceForm } from 'features/surveys/components/SurveyFundingSourceForm'; import { ISurveySiteSelectionForm } from 'features/surveys/components/SurveySiteSelectionForm'; import { Feature } from 'geojson'; @@ -19,10 +19,10 @@ import { ICritterDetailedResponse } from './useCritterApi.interface'; export interface ICreateSurveyRequest extends IGeneralInformationForm, IPurposeAndMethodologyForm, - IStudyAreaForm, IProprietaryDataForm, IAgreementsForm, IParticipantsJobForm, + ISurveyLocationForm, ISurveyBlockForm {} /** @@ -58,8 +58,6 @@ export interface IGetSurveyForViewResponseDetails { end_date: string; biologist_first_name: string; biologist_last_name: string; - survey_area_name: string; - geometry: Feature[]; survey_types: number[]; revision_count: number; } @@ -103,6 +101,16 @@ export interface IGetSurveyForUpdateResponsePartnerships { stakeholder_partnerships: string[]; } +export interface IGetSurveyLocation { + survey_location_id: number; + name: string; + description: string; + geometry: Feature[]; + geography: string | null; + geojson: Feature[]; + revision_count: number; +} + export interface SurveyViewObject { survey_details: IGetSurveyForViewResponseDetails; species: IGetSpecies; @@ -113,9 +121,10 @@ export interface SurveyViewObject { proprietor: IGetSurveyForViewResponseProprietor | null; participants: IGetSurveyParticipant[]; partnerships: IGetSurveyForViewResponsePartnerships; + locations: IGetSurveyLocation[]; } -export interface SurveyUpdateObject { +export interface SurveyUpdateObject extends ISurveyLocationForm { survey_details?: { survey_name: string; start_date: string; @@ -161,11 +170,6 @@ export interface SurveyUpdateObject { category_rationale: string; disa_required: StringBoolean; }; - location?: { - survey_area_name: string; - geometry: Feature[]; - revision_count: number; - }; participants?: { identity_source: string; email: string | null; @@ -339,7 +343,7 @@ export interface IDetailedCritterWithInternalId extends ICritterDetailedResponse export type IEditSurveyRequest = IGeneralInformationForm & IPurposeAndMethodologyForm & ISurveyFundingSourceForm & - IStudyAreaForm & + ISurveyLocationForm & IProprietaryDataForm & IUpdateAgreementsForm & { partnerships: IGetSurveyForViewResponsePartnerships } & ISurveySiteSelectionForm & IParticipantsJobForm; diff --git a/app/src/test-helpers/survey-helpers.ts b/app/src/test-helpers/survey-helpers.ts index a9087bbfa2..20c8ecab18 100644 --- a/app/src/test-helpers/survey-helpers.ts +++ b/app/src/test-helpers/survey-helpers.ts @@ -17,8 +17,6 @@ export const surveyObject: SurveyViewObject = { end_date: '2021-02-26', biologist_first_name: 'first', biologist_last_name: 'last', - survey_area_name: 'study area', - geometry: [geoJsonFeature], survey_types: [1], revision_count: 0 }, @@ -83,6 +81,17 @@ export const surveyObject: SurveyViewObject = { survey_job_id: 1, survey_job_name: 'survey job name' } + ], + locations: [ + { + survey_location_id: 1, + name: 'study area', + description: 'study area description', + geometry: [geoJsonFeature], + geography: null, + geojson: [geoJsonFeature], + revision_count: 0 + } ] }; diff --git a/database/src/migrations/20230802000000_new_funding_table.ts b/database/src/migrations/20230802000000_new_funding_table.ts index 5a4bebdae3..6d41bf8fef 100644 --- a/database/src/migrations/20230802000000_new_funding_table.ts +++ b/database/src/migrations/20230802000000_new_funding_table.ts @@ -1,7 +1,7 @@ import { Knex } from 'knex'; /** - * Added new program and project_program for tracking programs (used to be project type) + * Added new funding source and survey funding tables. * * @export * @param {Knex} knex diff --git a/database/src/migrations/20230831111300_survey_locations.ts b/database/src/migrations/20230831111300_survey_locations.ts new file mode 100644 index 0000000000..e352ff8ec9 --- /dev/null +++ b/database/src/migrations/20230831111300_survey_locations.ts @@ -0,0 +1,129 @@ +import { Knex } from 'knex'; + +/** + * Replace/enhance the old (unused) survey_location table. + * Migrate existing spatial data from survey table into new survey_location table. + * Drop old spatial data columns from survey table. + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + ------------------------------------------------------------------------- + -- Drop views/tables/constraints + ------------------------------------------------------------------------- + + SET SEARCH_PATH=biohub_dapi_v1; + + DROP VIEW IF EXISTS survey; + + DROP VIEW IF EXISTS survey_location; + + SET SEARCH_PATH=biohub,public; + + DROP TABLE IF EXISTS survey_location; + + ------------------------------------------------------------------------- + -- Create new tables + ------------------------------------------------------------------------- + + CREATE TABLE survey_location( + survey_location_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + survey_id integer NOT NULL, + name varchar(100) NOT NULL, + description varchar(250) NOT NULL, + geometry geometry(geometry, 3005), + geography geography(geometry) NOT NULL, + geojson jsonb NOT NULL, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT survey_location_pk PRIMARY KEY (survey_location_id) + ); + + COMMENT ON COLUMN survey_location.survey_location_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN survey_location.name IS 'The name of the spatial record.'; + COMMENT ON COLUMN survey_location.description IS 'The description of the spatial record.'; + COMMENT ON COLUMN survey_location.geometry IS 'The containing geometry of the record.'; + COMMENT ON COLUMN survey_location.geography IS 'The containing geography of the record.'; + COMMENT ON COLUMN survey_location.geojson IS 'A GeoJSON representation of the geometry which may contain additional metadata.'; + COMMENT ON COLUMN survey_location.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN survey_location.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN survey_location.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN survey_location.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN survey_location.revision_count IS 'Revision count used for concurrency control.'; + COMMENT ON TABLE survey_location IS 'Spatial records associated to a survey.'; + + ---------------------------------------------------------------------------------------- + -- Create Indexes and Constraints + ---------------------------------------------------------------------------------------- + + ALTER TABLE survey_location ADD CONSTRAINT survey_location_fk1 + FOREIGN KEY (survey_id) + REFERENCES survey(survey_id); + + -- Add indexes on foreign key columns + CREATE INDEX survey_location_idx1 ON survey_location(survey_id); + + ------------------------------------------------------------------------- + -- Create audit and journal triggers + ------------------------------------------------------------------------- + + CREATE TRIGGER audit_survey_location BEFORE INSERT OR UPDATE OR DELETE ON survey_location for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_survey_location AFTER INSERT OR UPDATE OR DELETE ON survey_location for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ---------------------------------------------------------------------------------------- + -- Migrate existing spatial data from survey table into new survey spatial components table + ---------------------------------------------------------------------------------------- + + INSERT INTO survey_location ( + survey_id, + name, + description, + geometry, + geography, + geojson, + create_date, + create_user + )( + select + survey.survey_id, + survey.location_name, + 'Insert description here' as description, + survey.geometry, + survey.geography, + survey.geojson, + survey.create_date, + survey.create_user + FROM + survey + ); + + ---------------------------------------------------------------------------------------- + -- Drop old spatial data columns from survey table + ---------------------------------------------------------------------------------------- + + ALTER TABLE survey DROP COLUMN geometry; + ALTER TABLE survey DROP COLUMN geography; + ALTER TABLE survey DROP COLUMN geojson; + ALTER TABLE survey DROP COLUMN location_description; + ALTER TABLE survey DROP COLUMN location_name; + + ---------------------------------------------------------------------------------------- + -- Create views + ---------------------------------------------------------------------------------------- + + SET SEARCH_PATH=biohub_dapi_v1; + + CREATE OR REPLACE VIEW survey_location AS SELECT * FROM biohub.survey_location; + CREATE OR REPLACE VIEW survey AS SELECT * FROM biohub.survey; + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +} diff --git a/database/src/seeds/03_basic_project_survey_setup.ts b/database/src/seeds/03_basic_project_survey_setup.ts index f8a6f740bc..7d3570c4fb 100644 --- a/database/src/seeds/03_basic_project_survey_setup.ts +++ b/database/src/seeds/03_basic_project_survey_setup.ts @@ -63,6 +63,7 @@ export async function seed(knex: Knex): Promise { ${insertSurveyStakeholderData(surveyId)} ${insertSurveyVantageData(surveyId)} ${insertSurveyParticipationData(surveyId)} + ${insertSurveyLocationData(surveyId)} `); } } @@ -240,35 +241,21 @@ const insertSurveyPermitData = (surveyId: number) => ` `; /** - * SQL to insert Survey data + * SQL to insert Survey location data * */ -const insertSurveyData = (projectId: number) => ` - INSERT into survey +const insertSurveyLocationData = (surveyId: number) => ` + INSERT into survey_location ( - project_id, + survey_id, name, - field_method_id, - additional_details, - start_date, - end_date, - lead_first_name, - lead_last_name, - location_name, + description, geography, - geojson, - ecological_season_id, - intended_outcome_id + geojson ) VALUES ( - ${projectId}, - 'Seed Survey', - (select field_method_id from field_method order by random() limit 1), - $$${faker.lorem.sentences(2)}$$, - $$${faker.date.between({ from: '2010-01-01T00:00:00-08:00', to: '2015-01-01T00:00:00-08:00' }).toISOString()}$$, - $$${faker.date.between({ from: '2020-01-01T00:00:00-08:00', to: '2025-01-01T00:00:00-08:00' }).toISOString()}$$, - $$${faker.person.firstName()}$$, - $$${faker.person.lastName()}$$, + ${surveyId}, + $$${faker.lorem.words(2)}$$, $$${faker.lorem.words(6)}$$, 'POLYGON ((-121.904297 50.930738, -121.904297 51.971346, -120.19043 51.971346, -120.19043 50.930738, -121.904297 50.930738))', '[ @@ -303,7 +290,37 @@ const insertSurveyData = (projectId: number) => ` }, "properties": {} } - ]', + ]' + ); +`; + +/** + * SQL to insert Survey data + * + */ +const insertSurveyData = (projectId: number) => ` + INSERT into survey + ( + project_id, + name, + field_method_id, + additional_details, + start_date, + end_date, + lead_first_name, + lead_last_name, + ecological_season_id, + intended_outcome_id + ) + VALUES ( + ${projectId}, + 'Seed Survey', + (select field_method_id from field_method order by random() limit 1), + $$${faker.lorem.sentences(2)}$$, + $$${faker.date.between({ from: '2010-01-01T00:00:00-08:00', to: '2015-01-01T00:00:00-08:00' }).toISOString()}$$, + $$${faker.date.between({ from: '2020-01-01T00:00:00-08:00', to: '2025-01-01T00:00:00-08:00' }).toISOString()}$$, + $$${faker.person.firstName()}$$, + $$${faker.person.lastName()}$$, (select ecological_season_id from ecological_season order by random() limit 1), (select intended_outcome_id from intended_outcome order by random() limit 1) )