From aef58dc2a4d6dff4b62bd0ea17b26296d7befb82 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 31 Aug 2023 13:59:41 -0700 Subject: [PATCH 01/20] stubbed out block repo --- api/src/repositories/block-repository.ts | 298 +++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 api/src/repositories/block-repository.ts diff --git a/api/src/repositories/block-repository.ts b/api/src/repositories/block-repository.ts new file mode 100644 index 0000000000..e7ba4c3362 --- /dev/null +++ b/api/src/repositories/block-repository.ts @@ -0,0 +1,298 @@ +import SQL from 'sql-template-strings'; +import { z } from 'zod'; +import { PROJECT_ROLE } from '../constants/roles'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { BaseRepository } from './base-repository'; +import { SystemUser } from './user-repository'; + +export const ProjectUser = z.object({ + project_participation_id: z.number(), + project_id: z.number(), + system_user_id: z.number(), + project_role_ids: z.array(z.number()), + project_role_names: z.array(z.string()), + project_role_permissions: z.array(z.string()) +}); + +export type ProjectUser = z.infer; + +export const SurveyBlockRecord = z.object({ + survey_block_id: z.number(), + survey_id: z.number(), + name: z.string(), + description: z.string().nullable(), + create_date: z.string(), + create_user: z.number(), + update_date: z.string().nullable(), + update_user: z.number().nullable(), + revision_count: z.number() +}); + +export type SurveyBlockRecord = z.infer; + +export interface IParticipant { + systemUserId: number; + userIdentifier: string; + identitySource: string; + roleId: number; + displayName: string; + email: string; +} + +export interface IInsertProjectParticipant { + system_user_id: number; + role: PROJECT_ROLE; +} + +/** + * A repository class for accessing project participants data. + * + * @export + * @class ProjectParticipationRepository + * @extends {BaseRepository} + */ +export class ProjectParticipationRepository extends BaseRepository { + /** + * Deletes a project participation record. + * + * @param {number} projectParticipationId + * @return {*} {Promise} + * @memberof ProjectParticipationRepository + */ + async deleteProjectParticipationRecord(projectParticipationId: number): Promise { + const sqlStatement = SQL` + DELETE FROM + project_participation + WHERE + project_participation_id = ${projectParticipationId} + RETURNING + *; + `; + + const response = await this.connection.sql(sqlStatement, ProjectParticipationRecord); + + if (!response || !response.rowCount) { + throw new ApiExecuteSQLError('Failed to delete project participation record', [ + 'ProjectRepository->deleteProjectParticipationRecord', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows[0]; + } + + async updateProjectParticipationRole(projectParticipationId: number, role: string): Promise { + const sql = SQL` + UPDATE project_participation + SET project_role_id = ( + SELECT project_role_id + FROM project_role + WHERE name = ${role} + AND record_end_date IS NULL + ) + WHERE project_participation_id = ${projectParticipationId}; + `; + await this.connection.sql(sql); + } + + /** + * Get a project user by project and system user id. Returns null if the system user is not a participant of the + * project. + * + * @param {number} projectId + * @param {number} systemUserId + * @return {*} {(Promise<(ProjectUser & SystemUser) | null>)} + * @memberof ProjectParticipationRepository + */ + async getProjectParticipant(projectId: number, systemUserId: number): Promise<(ProjectUser & SystemUser) | null> { + const sqlStatement = SQL` + SELECT + su.system_user_id, + su.user_identifier, + su.user_guid, + su.record_end_date, + uis.name AS identity_source, + array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, + array_remove(array_agg(sr.name), NULL) AS role_names, + su.email, + su.display_name, + su.agency, + pp.project_participation_id, + pp.project_id, + array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, + array_remove(array_agg(pr.name), NULL) AS project_role_names, + array_remove(array_agg(pp2.name), NULL) as project_role_permissions + FROM + project_participation pp + LEFT JOIN project_role pr + ON pp.project_role_id = pr.project_role_id + LEFT JOIN project_role_permission prp + ON pp.project_role_id = prp.project_role_id + LEFT JOIN project_permission pp2 + ON pp2.project_permission_id = prp.project_permission_id + LEFT JOIN system_user su + ON pp.system_user_id = su.system_user_id + LEFT JOIN + system_user_role sur + ON su.system_user_id = sur.system_user_id + LEFT JOIN + system_role sr + ON sur.system_role_id = sr.system_role_id + LEFT JOIN + user_identity_source uis + ON uis.user_identity_source_id = su.user_identity_source_id + WHERE + pp.project_id = ${projectId} + AND + pp.system_user_id = ${systemUserId} + AND + su.record_end_date is NULL + GROUP BY + su.system_user_id, + su.record_end_date, + su.user_identifier, + su.user_guid, + uis.name, + su.email, + su.display_name, + su.agency, + pp.project_participation_id, + pp.project_id; + `; + + const response = await this.connection.sql(sqlStatement, ProjectUser.merge(SystemUser)); + + return response.rows?.[0] || null; + } + + /** + * Gets a list of project participants for a given project. + * + * @param {number} projectId + * @return {*} {(Promise<(ProjectUser & SystemUser)[]>)} + * @memberof ProjectParticipationRepository + */ + async getProjectParticipants(projectId: number): Promise<(ProjectUser & SystemUser)[]> { + const sqlStatement = SQL` + SELECT + su.system_user_id, + su.user_identifier, + su.user_guid, + su.record_end_date, + uis.name AS identity_source, + array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, + array_remove(array_agg(sr.name), NULL) AS role_names, + su.email, + su.display_name, + su.agency, + pp.project_participation_id, + pp.project_id, + array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, + array_remove(array_agg(pr.name), NULL) AS project_role_names, + array_remove(array_agg(pp2.name), NULL) as project_role_permissions + FROM + project_participation pp + LEFT JOIN project_role pr + ON pp.project_role_id = pr.project_role_id + LEFT JOIN project_role_permission prp + ON pp.project_role_id = prp.project_role_id + LEFT JOIN project_permission pp2 + ON pp2.project_permission_id = prp.project_permission_id + LEFT JOIN system_user su + ON pp.system_user_id = su.system_user_id + LEFT JOIN + system_user_role sur + ON su.system_user_id = sur.system_user_id + LEFT JOIN + system_role sr + ON sur.system_role_id = sr.system_role_id + LEFT JOIN + user_identity_source uis + ON uis.user_identity_source_id = su.user_identity_source_id + WHERE + pp.project_id = ${projectId} + AND + su.record_end_date is NULL + GROUP BY + su.system_user_id, + su.record_end_date, + su.user_identifier, + su.user_guid, + uis.name, + su.email, + su.display_name, + su.agency, + pp.project_participation_id, + pp.project_id; + `; + + const response = await this.connection.sql(sqlStatement, ProjectUser.merge(SystemUser)); + + if (!response.rows.length) { + throw new ApiExecuteSQLError('Failed to get project team members', [ + 'ProjectRepository->getProjectParticipants', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows; + } + + /** + * Adds a project participant to the database. + * + * @param {number} projectId The ID of the project. + * @param {number} systemUserId The system ID of the user. + * @param {(number | string)} projectParticipantRole The ID or Name of the role to assign. + * @return {*} {Promise} + * @memberof ProjectParticipationRepository + */ + async postProjectParticipant( + projectId: number, + systemUserId: number, + projectParticipantRole: number | string + ): Promise { + let sqlStatement; + + if (isNaN(Number(projectParticipantRole))) { + sqlStatement = SQL` + INSERT INTO project_participation ( + project_id, + system_user_id, + project_role_id + ) + ( + SELECT + ${projectId}, + ${systemUserId}, + project_role_id + FROM + project_role + WHERE + name = ${projectParticipantRole} + ); + `; + } else { + sqlStatement = SQL` + INSERT INTO project_participation ( + project_id, + system_user_id, + project_role_id + ) VALUES ( + ${projectId}, + ${systemUserId}, + ${projectParticipantRole} + ); + `; + } + + const response = await this.connection.sql(sqlStatement); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to insert project team member', [ + 'ProjectRepository->postProjectParticipant', + 'rows was null or undefined, expected rows != null' + ]); + } + } +} From e86ceb6f3ecc9f5b7e6c39922c5d7fb24417d160 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 31 Aug 2023 16:12:16 -0700 Subject: [PATCH 02/20] renamed file, added block service --- api/src/repositories/block-repository.ts | 298 ------------------ .../repositories/survey-block-repository.ts | 107 +++++++ api/src/services/survey-block-service.ts | 55 ++++ 3 files changed, 162 insertions(+), 298 deletions(-) delete mode 100644 api/src/repositories/block-repository.ts create mode 100644 api/src/repositories/survey-block-repository.ts create mode 100644 api/src/services/survey-block-service.ts diff --git a/api/src/repositories/block-repository.ts b/api/src/repositories/block-repository.ts deleted file mode 100644 index e7ba4c3362..0000000000 --- a/api/src/repositories/block-repository.ts +++ /dev/null @@ -1,298 +0,0 @@ -import SQL from 'sql-template-strings'; -import { z } from 'zod'; -import { PROJECT_ROLE } from '../constants/roles'; -import { ApiExecuteSQLError } from '../errors/api-error'; -import { BaseRepository } from './base-repository'; -import { SystemUser } from './user-repository'; - -export const ProjectUser = z.object({ - project_participation_id: z.number(), - project_id: z.number(), - system_user_id: z.number(), - project_role_ids: z.array(z.number()), - project_role_names: z.array(z.string()), - project_role_permissions: z.array(z.string()) -}); - -export type ProjectUser = z.infer; - -export const SurveyBlockRecord = z.object({ - survey_block_id: z.number(), - survey_id: z.number(), - name: z.string(), - description: z.string().nullable(), - create_date: z.string(), - create_user: z.number(), - update_date: z.string().nullable(), - update_user: z.number().nullable(), - revision_count: z.number() -}); - -export type SurveyBlockRecord = z.infer; - -export interface IParticipant { - systemUserId: number; - userIdentifier: string; - identitySource: string; - roleId: number; - displayName: string; - email: string; -} - -export interface IInsertProjectParticipant { - system_user_id: number; - role: PROJECT_ROLE; -} - -/** - * A repository class for accessing project participants data. - * - * @export - * @class ProjectParticipationRepository - * @extends {BaseRepository} - */ -export class ProjectParticipationRepository extends BaseRepository { - /** - * Deletes a project participation record. - * - * @param {number} projectParticipationId - * @return {*} {Promise} - * @memberof ProjectParticipationRepository - */ - async deleteProjectParticipationRecord(projectParticipationId: number): Promise { - const sqlStatement = SQL` - DELETE FROM - project_participation - WHERE - project_participation_id = ${projectParticipationId} - RETURNING - *; - `; - - const response = await this.connection.sql(sqlStatement, ProjectParticipationRecord); - - if (!response || !response.rowCount) { - throw new ApiExecuteSQLError('Failed to delete project participation record', [ - 'ProjectRepository->deleteProjectParticipationRecord', - 'rows was null or undefined, expected rows != null' - ]); - } - - return response.rows[0]; - } - - async updateProjectParticipationRole(projectParticipationId: number, role: string): Promise { - const sql = SQL` - UPDATE project_participation - SET project_role_id = ( - SELECT project_role_id - FROM project_role - WHERE name = ${role} - AND record_end_date IS NULL - ) - WHERE project_participation_id = ${projectParticipationId}; - `; - await this.connection.sql(sql); - } - - /** - * Get a project user by project and system user id. Returns null if the system user is not a participant of the - * project. - * - * @param {number} projectId - * @param {number} systemUserId - * @return {*} {(Promise<(ProjectUser & SystemUser) | null>)} - * @memberof ProjectParticipationRepository - */ - async getProjectParticipant(projectId: number, systemUserId: number): Promise<(ProjectUser & SystemUser) | null> { - const sqlStatement = SQL` - SELECT - su.system_user_id, - su.user_identifier, - su.user_guid, - su.record_end_date, - uis.name AS identity_source, - array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, - array_remove(array_agg(sr.name), NULL) AS role_names, - su.email, - su.display_name, - su.agency, - pp.project_participation_id, - pp.project_id, - array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, - array_remove(array_agg(pr.name), NULL) AS project_role_names, - array_remove(array_agg(pp2.name), NULL) as project_role_permissions - FROM - project_participation pp - LEFT JOIN project_role pr - ON pp.project_role_id = pr.project_role_id - LEFT JOIN project_role_permission prp - ON pp.project_role_id = prp.project_role_id - LEFT JOIN project_permission pp2 - ON pp2.project_permission_id = prp.project_permission_id - LEFT JOIN system_user su - ON pp.system_user_id = su.system_user_id - LEFT JOIN - system_user_role sur - ON su.system_user_id = sur.system_user_id - LEFT JOIN - system_role sr - ON sur.system_role_id = sr.system_role_id - LEFT JOIN - user_identity_source uis - ON uis.user_identity_source_id = su.user_identity_source_id - WHERE - pp.project_id = ${projectId} - AND - pp.system_user_id = ${systemUserId} - AND - su.record_end_date is NULL - GROUP BY - su.system_user_id, - su.record_end_date, - su.user_identifier, - su.user_guid, - uis.name, - su.email, - su.display_name, - su.agency, - pp.project_participation_id, - pp.project_id; - `; - - const response = await this.connection.sql(sqlStatement, ProjectUser.merge(SystemUser)); - - return response.rows?.[0] || null; - } - - /** - * Gets a list of project participants for a given project. - * - * @param {number} projectId - * @return {*} {(Promise<(ProjectUser & SystemUser)[]>)} - * @memberof ProjectParticipationRepository - */ - async getProjectParticipants(projectId: number): Promise<(ProjectUser & SystemUser)[]> { - const sqlStatement = SQL` - SELECT - su.system_user_id, - su.user_identifier, - su.user_guid, - su.record_end_date, - uis.name AS identity_source, - array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, - array_remove(array_agg(sr.name), NULL) AS role_names, - su.email, - su.display_name, - su.agency, - pp.project_participation_id, - pp.project_id, - array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, - array_remove(array_agg(pr.name), NULL) AS project_role_names, - array_remove(array_agg(pp2.name), NULL) as project_role_permissions - FROM - project_participation pp - LEFT JOIN project_role pr - ON pp.project_role_id = pr.project_role_id - LEFT JOIN project_role_permission prp - ON pp.project_role_id = prp.project_role_id - LEFT JOIN project_permission pp2 - ON pp2.project_permission_id = prp.project_permission_id - LEFT JOIN system_user su - ON pp.system_user_id = su.system_user_id - LEFT JOIN - system_user_role sur - ON su.system_user_id = sur.system_user_id - LEFT JOIN - system_role sr - ON sur.system_role_id = sr.system_role_id - LEFT JOIN - user_identity_source uis - ON uis.user_identity_source_id = su.user_identity_source_id - WHERE - pp.project_id = ${projectId} - AND - su.record_end_date is NULL - GROUP BY - su.system_user_id, - su.record_end_date, - su.user_identifier, - su.user_guid, - uis.name, - su.email, - su.display_name, - su.agency, - pp.project_participation_id, - pp.project_id; - `; - - const response = await this.connection.sql(sqlStatement, ProjectUser.merge(SystemUser)); - - if (!response.rows.length) { - throw new ApiExecuteSQLError('Failed to get project team members', [ - 'ProjectRepository->getProjectParticipants', - 'rows was null or undefined, expected rows != null' - ]); - } - - return response.rows; - } - - /** - * Adds a project participant to the database. - * - * @param {number} projectId The ID of the project. - * @param {number} systemUserId The system ID of the user. - * @param {(number | string)} projectParticipantRole The ID or Name of the role to assign. - * @return {*} {Promise} - * @memberof ProjectParticipationRepository - */ - async postProjectParticipant( - projectId: number, - systemUserId: number, - projectParticipantRole: number | string - ): Promise { - let sqlStatement; - - if (isNaN(Number(projectParticipantRole))) { - sqlStatement = SQL` - INSERT INTO project_participation ( - project_id, - system_user_id, - project_role_id - ) - ( - SELECT - ${projectId}, - ${systemUserId}, - project_role_id - FROM - project_role - WHERE - name = ${projectParticipantRole} - ); - `; - } else { - sqlStatement = SQL` - INSERT INTO project_participation ( - project_id, - system_user_id, - project_role_id - ) VALUES ( - ${projectId}, - ${systemUserId}, - ${projectParticipantRole} - ); - `; - } - - const response = await this.connection.sql(sqlStatement); - - if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to insert project team member', [ - 'ProjectRepository->postProjectParticipant', - 'rows was null or undefined, expected rows != null' - ]); - } - } -} diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts new file mode 100644 index 0000000000..144c462b64 --- /dev/null +++ b/api/src/repositories/survey-block-repository.ts @@ -0,0 +1,107 @@ +import SQL from 'sql-template-strings'; +import { z } from 'zod'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { BaseRepository } from './base-repository'; + +export const SurveyBlock = z.object({ + survey_block_id: z.number().nullable(), + survey_id: z.number(), + name: z.string(), + description: z.string() +}); + +export type SurveyBlock = z.infer; + +// This describes the a row in the database for Survey Block +export const SurveyBlockRecord = z.object({ + survey_block_id: z.number(), + survey_id: z.number(), + name: z.string(), + description: z.string(), + create_date: z.string(), + create_user: z.number(), + update_date: z.string().nullable(), + update_user: z.number().nullable(), + revision_count: z.number() +}); +export type SurveyBlockRecord = z.infer; + +/** + * A repository class for accessing project participants data. + * + * @export + * @class ProjectParticipationRepository + * @extends {BaseRepository} + */ +export class SurveyBlockRepository extends BaseRepository { + async getSurveyBlocksForSurveyId(surveyId: number): Promise { + const sql = SQL` + SELECT * + FROM survey_block + WHERE survey_id = ${surveyId}; + `; + + const response = await this.connection.sql(sql, SurveyBlockRecord); + return response.rows || []; + } + + async updateSurveyBlock(block: SurveyBlock): Promise { + const sql = SQL` + UPDATE survey_block SET name = ${block.name}, description = ${block.description}, survey_id=${block.survey_id} WHERE survey_block_id = ${block.survey_block_id}; + `; + await this.connection.sql(sql); + } + + async insertSurveyBlock(block: SurveyBlock): Promise { + const sql = SQL` + INSERT INTO survey_block ( + survey_id, + name, + description + ) VALUES ( + ${block.survey_id}, + ${block.name}, + ${block.description} + ); + `; + const response = await this.connection.sql(sql, SurveyBlockRecord); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to insert project team member', [ + 'SurveyBlockRepository->postSurveyBlock', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows[0]; + } + + /** + * Deletes a survey block record. + * + * @param {number} surveyBlockId + * @return {*} {Promise} + * @memberof SurveyBlockRepository + */ + async deleteSurveyBlockRecord(surveyBlockId: number): Promise { + const sqlStatement = SQL` + DELETE FROM + survey_block + WHERE + survey_id = ${surveyBlockId} + RETURNING + *; + `; + + const response = await this.connection.sql(sqlStatement, SurveyBlockRecord); + + if (!response || !response.rowCount) { + throw new ApiExecuteSQLError('Failed to delete survey block record', [ + 'SurveyBlockRepository->deleteSurveyBlockRecord', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows[0]; + } +} diff --git a/api/src/services/survey-block-service.ts b/api/src/services/survey-block-service.ts new file mode 100644 index 0000000000..ca3e88e871 --- /dev/null +++ b/api/src/services/survey-block-service.ts @@ -0,0 +1,55 @@ +import { IDBConnection } from '../database/db'; +import { SurveyBlock, SurveyBlockRecord, SurveyBlockRepository } from '../repositories/survey-block-repository'; +import { DBService } from './db-service'; + +// const defaultLog = getLogger('services/survey-block-service'); + +export class SurveyBlockService extends DBService { + surveyBlockRepository: SurveyBlockRepository; + + constructor(connection: IDBConnection) { + super(connection); + this.surveyBlockRepository = new SurveyBlockRepository(connection); + } + + async getSurveyBlocksForSurveyId(surveyId: number): Promise { + return await this.surveyBlockRepository.getSurveyBlocksForSurveyId(surveyId); + } + + async deleteSurveyBlock(surveyBlockId: number): Promise { + return this.surveyBlockRepository.deleteSurveyBlockRecord(surveyBlockId); + } + + async updateInsertSurveyBlocks(blocks: SurveyBlock[]): Promise { + const insertUpdate: Promise[] = []; + + blocks.forEach((item: SurveyBlock) => { + if (item.survey_block_id) { + insertUpdate.push(this.surveyBlockRepository.updateSurveyBlock(item)); + } else { + insertUpdate.push(this.surveyBlockRepository.insertSurveyBlock(item)); + } + }); + + await Promise.all(insertUpdate); + } + + async upsertSurveyBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { + // all actions to take + const promises: Promise[] = []; + // get old + // delete not found + // upsert the last + const existingBlocks = await this.getSurveyBlocksForSurveyId(surveyId); + const blocksToDelete = existingBlocks.filter( + (item) => !blocks.find((incoming) => incoming.survey_block_id === item.survey_block_id) + ); + blocksToDelete.forEach((item) => { + promises.push(this.deleteSurveyBlock(item.survey_block_id)); + }); + + promises.push(this.updateInsertSurveyBlocks(blocks)); + + await Promise.all(promises); + } +} From 9b255aa7db40f54f2aa00949b37d0d057eebea24 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 31 Aug 2023 16:40:21 -0700 Subject: [PATCH 03/20] updated survey service to handle blocks --- api/src/models/survey-create.ts | 3 +++ api/src/models/survey-update.ts | 3 +++ .../project/{projectId}/survey/create.ts | 18 +++++++++++++++ .../{projectId}/survey/{surveyId}/update.ts | 22 +++++++++++++++++++ .../repositories/survey-block-repository.ts | 4 +++- api/src/services/survey-service.ts | 15 +++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/api/src/models/survey-create.ts b/api/src/models/survey-create.ts index 77a59a500b..02ccaeaf15 100644 --- a/api/src/models/survey-create.ts +++ b/api/src/models/survey-create.ts @@ -1,4 +1,5 @@ import { Feature } from 'geojson'; +import { SurveyBlock } from '../repositories/survey-block-repository'; export class PostSurveyObject { survey_details: PostSurveyDetailsData; @@ -11,6 +12,7 @@ export class PostSurveyObject { agreements: PostAgreementsData; participants: PostParticipationData[]; partnerships: PostPartnershipsData; + blocks: SurveyBlock[]; constructor(obj?: any) { this.survey_details = (obj?.survey_details && new PostSurveyDetailsData(obj.survey_details)) || null; @@ -26,6 +28,7 @@ export class PostSurveyObject { this.participants = (obj?.participants?.length && obj.participants.map((p: any) => new PostParticipationData(p))) || []; this.partnerships = (obj?.partnerships && new PostPartnershipsData(obj.partnerships)) || null; + this.blocks = (obj?.blocks && obj.blocks.map((p: any) => p as SurveyBlock)) || []; } } diff --git a/api/src/models/survey-update.ts b/api/src/models/survey-update.ts index 502f526655..9fb4dcdc9a 100644 --- a/api/src/models/survey-update.ts +++ b/api/src/models/survey-update.ts @@ -1,4 +1,5 @@ import { Feature } from 'geojson'; +import { SurveyBlock } from '../repositories/survey-block-repository'; export class PutSurveyObject { survey_details: PutSurveyDetailsData; @@ -10,6 +11,7 @@ export class PutSurveyObject { location: PutSurveyLocationData; participants: PutSurveyParticipantsData[]; partnerships: PutPartnershipsData; + blocks: SurveyBlock[]; constructor(obj?: any) { this.survey_details = (obj?.survey_details && new PutSurveyDetailsData(obj.survey_details)) || null; @@ -24,6 +26,7 @@ export class PutSurveyObject { this.participants = (obj?.participants?.length && obj.participants.map((p: any) => new PutSurveyParticipantsData(p))) || []; this.partnerships = (obj?.partnerships && new PutPartnershipsData(obj.partnerships)) || null; + this.blocks = (obj?.blocks && obj.blocks.map((p: any) => p as SurveyBlock)) || []; } } diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index ff1984b304..2b4593d42e 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -248,6 +248,24 @@ POST.apiDoc = { } } } + }, + blocks: { + type: 'array', + items: { + type: 'object', + required: ['survey_id', 'name', 'description'], + properties: { + survey_id: { + type: 'number' + }, + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } } } } diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index 26571e3a1c..c5c71a0651 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -305,6 +305,28 @@ PUT.apiDoc = { } } } + }, + blocks: { + type: 'array', + items: { + type: 'object', + required: ['survey_id', 'name', 'description'], + properties: { + survey_block_id: { + type: 'number', + nullable: true + }, + survey_id: { + type: 'number' + }, + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } } } } diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index 144c462b64..8dd3ee28af 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -62,7 +62,9 @@ export class SurveyBlockRepository extends BaseRepository { ${block.survey_id}, ${block.name}, ${block.description} - ); + ) + RETURNING + *; `; const response = await this.connection.sql(sql, SurveyBlockRecord); diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index a067280423..41f1516955 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -20,6 +20,7 @@ import { } from '../models/survey-view'; import { AttachmentRepository } from '../repositories/attachment-repository'; import { PublishStatus } from '../repositories/history-publish-repository'; +import { SurveyBlock } from '../repositories/survey-block-repository'; import { IGetLatestSurveyOccurrenceSubmission, IObservationSubmissionInsertDetails, @@ -35,6 +36,7 @@ import { HistoryPublishService } from './history-publish-service'; import { PermitService } from './permit-service'; import { PlatformService } from './platform-service'; import { RegionService } from './region-service'; +import { SurveyBlockService } from './survey-block-service'; import { SurveyParticipationService } from './survey-participation-service'; import { TaxonomyService } from './taxonomy-service'; @@ -445,11 +447,20 @@ export class SurveyService extends DBService { promises.push(this.insertRegion(surveyId, postSurveyData.location.geometry)); } + if (postSurveyData.blocks) { + promises.push(this.upsertBlocks(surveyId, postSurveyData.blocks)); + } + await Promise.all(promises); return surveyId; } + async upsertBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { + const service = new SurveyBlockService(this.connection); + return service.upsertSurveyBlocks(surveyId, blocks); + } + async insertRegion(projectId: number, features: Feature[]): Promise { const regionService = new RegionService(this.connection); return regionService.addRegionsToSurveyFromFeatures(projectId, features); @@ -654,6 +665,10 @@ export class SurveyService extends DBService { promises.push(this.upsertSurveyParticipantData(surveyId, putSurveyData)); } + if (putSurveyData?.blocks) { + promises.push(this.upsertBlocks(surveyId, putSurveyData.blocks)); + } + await Promise.all(promises); } From bf2a1dac512c0f1869235b99d23ff364679e1f9d Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Sep 2023 14:38:18 -0700 Subject: [PATCH 04/20] stubbing out new block UI --- app/src/features/surveys/CreateSurveyPage.tsx | 11 ++++++++++- app/src/interfaces/useSurveyApi.interface.ts | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index 5f144316ca..a45ec09068 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -43,6 +43,7 @@ import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from './components/PurposeAndMethodologyForm'; import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from './components/StudyAreaForm'; +import SurveyBlockSection, { SurveyBlockInitialValues } from './components/SurveyBlockSection'; import SurveyFundingSourceForm, { SurveyFundingSourceFormInitialValues, SurveyFundingSourceFormYupSchema @@ -136,7 +137,8 @@ const CreateSurveyPage = () => { ...SurveyPartnershipsFormInitialValues, ...ProprietaryDataInitialValues, ...AgreementsInitialValues, - ...SurveyUserJobFormInitialValues + ...SurveyUserJobFormInitialValues, + ...SurveyBlockInitialValues }); // Yup schemas for the survey form sections @@ -377,6 +379,13 @@ const CreateSurveyPage = () => { + } + /> + + Date: Fri, 1 Sep 2023 15:36:47 -0700 Subject: [PATCH 05/20] basic block UI stubbed out --- .../features/surveys/components/BlockForm.tsx | 38 +++++ .../components/CreateSurveyBlockDialog.tsx | 39 +++++ .../components/EditSurveyBlockDialog.tsx | 40 +++++ .../surveys/components/SurveyBlockSection.tsx | 152 ++++++++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 app/src/features/surveys/components/BlockForm.tsx create mode 100644 app/src/features/surveys/components/CreateSurveyBlockDialog.tsx create mode 100644 app/src/features/surveys/components/EditSurveyBlockDialog.tsx create mode 100644 app/src/features/surveys/components/SurveyBlockSection.tsx diff --git a/app/src/features/surveys/components/BlockForm.tsx b/app/src/features/surveys/components/BlockForm.tsx new file mode 100644 index 0000000000..bb1e68b7b8 --- /dev/null +++ b/app/src/features/surveys/components/BlockForm.tsx @@ -0,0 +1,38 @@ +import Typography from '@mui/material/Typography'; +import { Box } from '@mui/system'; +import CustomTextField from 'components/fields/CustomTextField'; +import React from 'react'; + +export interface IBlockData { + survey_block_id: number | null; + name: string; + description: string; +} + +const BlockForm: React.FC = () => { + return ( +
+ + + Name and Description + + + + + + +
+ ); +}; + +export default BlockForm; diff --git a/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx b/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx new file mode 100644 index 0000000000..875a6172ce --- /dev/null +++ b/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx @@ -0,0 +1,39 @@ +import EditDialog from 'components/dialog/EditDialog'; +import BlockForm from './BlockForm'; +import { BlockYupSchema } from './SurveyBlockSection'; + +interface ICreateBlockProps { + open: boolean; + onSave: (data: any) => void; + onClose: () => void; +} + +const CreateSurveyBlockDialog: React.FC = (props) => { + const { open, onSave, onClose } = props; + return ( + <> + , + initialValues: { + survey_block_id: null, + name: '', + description: '' + }, + validationSchema: BlockYupSchema + }} + dialogSaveButtonLabel="Add Block" + onCancel={() => onClose()} + onSave={(formValues) => onSave(formValues)} + /> + + ); +}; + +export default CreateSurveyBlockDialog; diff --git a/app/src/features/surveys/components/EditSurveyBlockDialog.tsx b/app/src/features/surveys/components/EditSurveyBlockDialog.tsx new file mode 100644 index 0000000000..20049585fa --- /dev/null +++ b/app/src/features/surveys/components/EditSurveyBlockDialog.tsx @@ -0,0 +1,40 @@ +import EditDialog from 'components/dialog/EditDialog'; +import BlockForm from './BlockForm'; +import { BlockYupSchema, IEditBlock } from './SurveyBlockSection'; + +interface IEditBlockProps { + open: boolean; + initialData?: IEditBlock; + onSave: (data: any, index?: number) => void; + onClose: () => void; +} + +const EditSurveyBlockDialog: React.FC = (props) => { + const { open, initialData, onSave, onClose } = props; + return ( + <> + , + initialValues: { + survey_block_id: initialData?.block.survey_block_id || null, + name: initialData?.block.name || '', + description: initialData?.block.description || '' + }, + validationSchema: BlockYupSchema + }} + dialogSaveButtonLabel="Save" + onCancel={() => onClose()} + onSave={(formValues) => onSave(formValues, initialData?.index)} + /> + + ); +}; + +export default EditSurveyBlockDialog; diff --git a/app/src/features/surveys/components/SurveyBlockSection.tsx b/app/src/features/surveys/components/SurveyBlockSection.tsx new file mode 100644 index 0000000000..6555327003 --- /dev/null +++ b/app/src/features/surveys/components/SurveyBlockSection.tsx @@ -0,0 +1,152 @@ +import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +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 IconButton from '@mui/material/IconButton'; +import { useFormikContext } from 'formik'; +import { ICreateSurveyRequest } from 'interfaces/useSurveyApi.interface'; +import React, { useState } from 'react'; +import yup from 'utils/YupSchema'; +import CreateSurveyBlockDialog from './CreateSurveyBlockDialog'; +import EditSurveyBlockDialog from './EditSurveyBlockDialog'; + +interface ISurveyBlockFormProps { + name: string; // the name of the formik field value +} + +export const SurveyBlockInitialValues = { + blocks: [] +}; + +// Form validation for Block Item +export const BlockYupSchema = yup.object({ + name: yup.string().required(), + description: yup.string().required() +}); + +export const SurveyBlockYupSchema = yup.array(BlockYupSchema); + +export interface IEditBlock { + index: number; + block: { + survey_block_id: number | null; + name: string; + description: string; + }; +} + +const SurveyBlockSection: React.FC = (props) => { + const { name } = props; + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const [editData, setEditData] = useState(undefined); + + const formikProps = useFormikContext(); + const { values, handleSubmit, setFieldValue } = formikProps; + + const handleMenuClick = (event: React.MouseEvent, index: number) => { + setAnchorEl(event.currentTarget); + setEditData({ index: index, block: values.blocks[index] }); + }; + + const handleDelete = () => { + if (editData?.index) { + const data = values.blocks; + data.splice(editData.index, 1); + setFieldValue(name, data); + } + setAnchorEl(null); + }; + + // NEED TO UPDATE THE SERVICE FILES + // CREATE ACTION WE WON"T HAVE ANY SURVEY IDS so I need to update everything to reflect that + return ( + <> + {/* CREATE BLOCK DIALOG */} + setIsCreateModalOpen(false)} + onSave={(data) => { + setFieldValue(`${name}[${values.blocks.length}]`, data); + setIsCreateModalOpen(false); + setEditData(undefined); + }} + /> + + {/* EDIT BLOCK DIALOG */} + setIsEditModalOpen(false)} + onSave={(data, index) => { + setFieldValue(`${name}[${index}]`, data); + setIsEditModalOpen(false); + setEditData(undefined); + }} + /> + setAnchorEl(null)} + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right' + }}> + setIsEditModalOpen(true)}> + + + + Edit Details + + handleDelete()}> + + + + Remove + + +
+ + + {values.blocks.map((item, index) => { + return ( + + + + + } + onClick={(event: React.MouseEvent) => handleMenuClick(event, index)} + title={item.name} + subheader={item.description} + /> + + ); + })} + +
+ + ); +}; + +export default SurveyBlockSection; From c917db0dbc789fc4918fc887218af7e7083252c2 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Sep 2023 16:04:05 -0700 Subject: [PATCH 06/20] block saving on create --- api/src/paths/project/{projectId}/survey/create.ts | 5 +---- api/src/services/survey-block-service.ts | 5 +++-- .../surveys/components/SurveyBlockSection.tsx | 12 +++++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 2b4593d42e..0115b203aa 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -253,11 +253,8 @@ POST.apiDoc = { type: 'array', items: { type: 'object', - required: ['survey_id', 'name', 'description'], + required: ['name', 'description'], properties: { - survey_id: { - type: 'number' - }, name: { type: 'string' }, diff --git a/api/src/services/survey-block-service.ts b/api/src/services/survey-block-service.ts index ca3e88e871..a672b7366e 100644 --- a/api/src/services/survey-block-service.ts +++ b/api/src/services/survey-block-service.ts @@ -20,10 +20,11 @@ export class SurveyBlockService extends DBService { return this.surveyBlockRepository.deleteSurveyBlockRecord(surveyBlockId); } - async updateInsertSurveyBlocks(blocks: SurveyBlock[]): Promise { + async updateInsertSurveyBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { const insertUpdate: Promise[] = []; blocks.forEach((item: SurveyBlock) => { + item.survey_id = surveyId; if (item.survey_block_id) { insertUpdate.push(this.surveyBlockRepository.updateSurveyBlock(item)); } else { @@ -48,7 +49,7 @@ export class SurveyBlockService extends DBService { promises.push(this.deleteSurveyBlock(item.survey_block_id)); }); - promises.push(this.updateInsertSurveyBlocks(blocks)); + promises.push(this.updateInsertSurveyBlocks(surveyId, blocks)); await Promise.all(promises); } diff --git a/app/src/features/surveys/components/SurveyBlockSection.tsx b/app/src/features/surveys/components/SurveyBlockSection.tsx index 6555327003..117fff557f 100644 --- a/app/src/features/surveys/components/SurveyBlockSection.tsx +++ b/app/src/features/surveys/components/SurveyBlockSection.tsx @@ -63,8 +63,6 @@ const SurveyBlockSection: React.FC = (props) => { setAnchorEl(null); }; - // NEED TO UPDATE THE SERVICE FILES - // CREATE ACTION WE WON"T HAVE ANY SURVEY IDS so I need to update everything to reflect that return ( <> {/* CREATE BLOCK DIALOG */} @@ -72,9 +70,9 @@ const SurveyBlockSection: React.FC = (props) => { open={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} onSave={(data) => { + setEditData(undefined); setFieldValue(`${name}[${values.blocks.length}]`, data); setIsCreateModalOpen(false); - setEditData(undefined); }} /> @@ -82,11 +80,15 @@ const SurveyBlockSection: React.FC = (props) => { setIsEditModalOpen(false)} + onClose={() => { + setIsEditModalOpen(false); + setAnchorEl(null); + }} onSave={(data, index) => { + setEditData(undefined); setFieldValue(`${name}[${index}]`, data); setIsEditModalOpen(false); - setEditData(undefined); + setAnchorEl(null); }} /> Date: Tue, 5 Sep 2023 09:32:25 -0700 Subject: [PATCH 07/20] added block to survey edit page/ update apis --- api/src/models/survey-view.ts | 2 ++ .../{projectId}/survey/{surveyId}/update.ts | 5 +---- .../survey/{surveyId}/update/get.ts | 18 ++++++++++++++++++ .../repositories/survey-block-repository.ts | 2 +- api/src/services/survey-block-service.ts | 1 + api/src/services/survey-service.ts | 10 ++++++++-- .../surveys/components/SurveyBlockSection.tsx | 2 +- .../features/surveys/edit/EditSurveyForm.tsx | 11 ++++++++++- 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index ef04263302..5358b3c3a0 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -1,6 +1,7 @@ import { Feature } from 'geojson'; import { SurveyMetadataPublish } from '../repositories/history-publish-repository'; import { IPermitModel } from '../repositories/permit-repository'; +import { SurveyBlockRecord } from '../repositories/survey-block-repository'; import { SurveyUser } from '../repositories/survey-participation-repository'; export type SurveyObject = { @@ -13,6 +14,7 @@ export type SurveyObject = { location: GetSurveyLocationData; participants: SurveyUser[]; partnerships: ISurveyPartnerships; + blocks: SurveyBlockRecord[]; }; export interface ISurveyPartnerships { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index c5c71a0651..d1a7b49fb9 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -310,15 +310,12 @@ PUT.apiDoc = { type: 'array', items: { type: 'object', - required: ['survey_id', 'name', 'description'], + required: ['name', 'description'], properties: { survey_block_id: { type: 'number', nullable: true }, - survey_id: { - type: 'number' - }, name: { type: 'string' }, 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 bf9a2acc44..6888f4f60f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts @@ -341,6 +341,24 @@ GET.apiDoc = { } } } + }, + blocks: { + type: 'array', + items: { + type: 'object', + required: ['survey_block_id', 'name', 'description'], + properties: { + survey_block_id: { + type: 'number' + }, + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } } } } diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index 8dd3ee28af..57798467f8 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -90,7 +90,7 @@ export class SurveyBlockRepository extends BaseRepository { DELETE FROM survey_block WHERE - survey_id = ${surveyBlockId} + survey_block_id = ${surveyBlockId} RETURNING *; `; diff --git a/api/src/services/survey-block-service.ts b/api/src/services/survey-block-service.ts index a672b7366e..9d4c89b2ae 100644 --- a/api/src/services/survey-block-service.ts +++ b/api/src/services/survey-block-service.ts @@ -45,6 +45,7 @@ export class SurveyBlockService extends DBService { const blocksToDelete = existingBlocks.filter( (item) => !blocks.find((incoming) => incoming.survey_block_id === item.survey_block_id) ); + blocksToDelete.forEach((item) => { promises.push(this.deleteSurveyBlock(item.survey_block_id)); }); diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 41f1516955..2fed4e459e 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -20,7 +20,7 @@ import { } from '../models/survey-view'; import { AttachmentRepository } from '../repositories/attachment-repository'; import { PublishStatus } from '../repositories/history-publish-repository'; -import { SurveyBlock } from '../repositories/survey-block-repository'; +import { SurveyBlock, SurveyBlockRecord } from '../repositories/survey-block-repository'; import { IGetLatestSurveyOccurrenceSubmission, IObservationSubmissionInsertDetails, @@ -96,10 +96,16 @@ export class SurveyService extends DBService { purpose_and_methodology: await this.getSurveyPurposeAndMethodology(surveyId), proprietor: await this.getSurveyProprietorDataForView(surveyId), location: await this.getSurveyLocationData(surveyId), - participants: await this.surveyParticipationService.getSurveyParticipants(surveyId) + participants: await this.surveyParticipationService.getSurveyParticipants(surveyId), + blocks: await this.getSurveyBlocksForSurveyId(surveyId) }; } + async getSurveyBlocksForSurveyId(surveyId: number): Promise { + const service = new SurveyBlockService(this.connection); + return service.getSurveyBlocksForSurveyId(surveyId); + } + async getSurveyPartnershipsData(surveyId: number): Promise { const [indigenousPartnerships, stakeholderPartnerships] = [ await this.surveyRepository.getIndigenousPartnershipsBySurveyId(surveyId), diff --git a/app/src/features/surveys/components/SurveyBlockSection.tsx b/app/src/features/surveys/components/SurveyBlockSection.tsx index 117fff557f..8cc41d44f9 100644 --- a/app/src/features/surveys/components/SurveyBlockSection.tsx +++ b/app/src/features/surveys/components/SurveyBlockSection.tsx @@ -55,7 +55,7 @@ const SurveyBlockSection: React.FC = (props) => { }; const handleDelete = () => { - if (editData?.index) { + if (editData) { const data = values.blocks; data.splice(editData.index, 1); setFieldValue(name, data); diff --git a/app/src/features/surveys/edit/EditSurveyForm.tsx b/app/src/features/surveys/edit/EditSurveyForm.tsx index ce38478e49..c9d2debf33 100644 --- a/app/src/features/surveys/edit/EditSurveyForm.tsx +++ b/app/src/features/surveys/edit/EditSurveyForm.tsx @@ -28,6 +28,7 @@ import GeneralInformationForm, { import ProprietaryDataForm, { ProprietaryDataYupSchema } from '../components/ProprietaryDataForm'; import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from '../components/PurposeAndMethodologyForm'; import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from '../components/StudyAreaForm'; +import SurveyBlockSection, { SurveyBlockInitialValues } from '../components/SurveyBlockSection'; import SurveyFundingSourceForm, { SurveyFundingSourceFormInitialValues, SurveyFundingSourceFormYupSchema @@ -96,7 +97,8 @@ const EditSurveyForm: React.FC = (props) => { foippa_requirements_accepted: 'true' as unknown as StringBoolean } }, - ...SurveyUserJobFormInitialValues + ...SurveyUserJobFormInitialValues, + ...SurveyBlockInitialValues }); // Yup schemas for the survey form sections @@ -233,6 +235,13 @@ const EditSurveyForm: React.FC = (props) => { + } + /> + + Date: Tue, 5 Sep 2023 10:47:36 -0700 Subject: [PATCH 08/20] adding js docs --- .../repositories/survey-block-repository.ts | 27 ++++++++++-- api/src/services/survey-block-service.ts | 42 ++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index 57798467f8..4c7c791aed 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -27,13 +27,20 @@ export const SurveyBlockRecord = z.object({ export type SurveyBlockRecord = z.infer; /** - * A repository class for accessing project participants data. + * A repository class for accessing Survey Block data. * * @export - * @class ProjectParticipationRepository + * @class SurveyBlockRepository * @extends {BaseRepository} */ export class SurveyBlockRepository extends BaseRepository { + /** + * Gets all Survey Block Records for a given survey id. + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof SurveyBlockRepository + */ async getSurveyBlocksForSurveyId(surveyId: number): Promise { const sql = SQL` SELECT * @@ -45,6 +52,13 @@ export class SurveyBlockRepository extends BaseRepository { return response.rows || []; } + /** + * Updates a survey block record. + * + * @param {SurveyBlock} block + * @return {*} {Promise} + * @memberof SurveyBlockRepository + */ async updateSurveyBlock(block: SurveyBlock): Promise { const sql = SQL` UPDATE survey_block SET name = ${block.name}, description = ${block.description}, survey_id=${block.survey_id} WHERE survey_block_id = ${block.survey_block_id}; @@ -52,6 +66,13 @@ export class SurveyBlockRepository extends BaseRepository { await this.connection.sql(sql); } + /** + * Inserts a survey block record. + * + * @param {SurveyBlock} block + * @return {*} {Promise} + * @memberof SurveyBlockRepository + */ async insertSurveyBlock(block: SurveyBlock): Promise { const sql = SQL` INSERT INTO survey_block ( @@ -79,7 +100,7 @@ export class SurveyBlockRepository extends BaseRepository { } /** - * Deletes a survey block record. + * Deletes a survey block record. * * @param {number} surveyBlockId * @return {*} {Promise} diff --git a/api/src/services/survey-block-service.ts b/api/src/services/survey-block-service.ts index 9d4c89b2ae..67852c8db9 100644 --- a/api/src/services/survey-block-service.ts +++ b/api/src/services/survey-block-service.ts @@ -2,8 +2,6 @@ import { IDBConnection } from '../database/db'; import { SurveyBlock, SurveyBlockRecord, SurveyBlockRepository } from '../repositories/survey-block-repository'; import { DBService } from './db-service'; -// const defaultLog = getLogger('services/survey-block-service'); - export class SurveyBlockService extends DBService { surveyBlockRepository: SurveyBlockRepository; @@ -12,14 +10,36 @@ export class SurveyBlockService extends DBService { this.surveyBlockRepository = new SurveyBlockRepository(connection); } + /** + * Gets Block Survey Records for a given survey id + * + * @param {number} surveyId + * @return {*} {Promise} + * @returns + */ async getSurveyBlocksForSurveyId(surveyId: number): Promise { return await this.surveyBlockRepository.getSurveyBlocksForSurveyId(surveyId); } + /** + * Deletes a survey block record. + * + * @param {number} surveyBlockId + * @return {*} {Promise} + * @memberof SurveyBlockService + */ async deleteSurveyBlock(surveyBlockId: number): Promise { return this.surveyBlockRepository.deleteSurveyBlockRecord(surveyBlockId); } + /** + * Insert or Updates Survey Block Records based on the existence of a survey_block_id + * + * @param {number} surveyId + * @param {SurveyBlock[]} blocks + * @return {*} {Promise} + * @memberof SurveyBlockService + */ async updateInsertSurveyBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { const insertUpdate: Promise[] = []; @@ -35,13 +55,24 @@ export class SurveyBlockService extends DBService { await Promise.all(insertUpdate); } + /** + * Inserts, Updates and Deletes Block records + * All passed in blocks are treated as the source of truth, + * Any pre existing blocks that do not collide with passed in blocks are deleted + * + * @param {number} surveyId + * @param {SurveyBlock[]} blocks + * @return {*} {Promise} + * @memberof SurveyBlockService + */ async upsertSurveyBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { // all actions to take const promises: Promise[] = []; - // get old - // delete not found - // upsert the last + + // Get existing blocks const existingBlocks = await this.getSurveyBlocksForSurveyId(surveyId); + + // Filter out any const blocksToDelete = existingBlocks.filter( (item) => !blocks.find((incoming) => incoming.survey_block_id === item.survey_block_id) ); @@ -50,6 +81,7 @@ export class SurveyBlockService extends DBService { promises.push(this.deleteSurveyBlock(item.survey_block_id)); }); + // update or insert block data promises.push(this.updateInsertSurveyBlocks(surveyId, blocks)); await Promise.all(promises); From 5fd0a9c90fdc808e5b1f0585f6b665339b4e0e3c Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Sep 2023 13:42:46 -0700 Subject: [PATCH 09/20] added survey block repo tests --- .../survey-block-repository.test.ts | 156 ++++++++++++++++++ .../repositories/survey-block-repository.ts | 10 +- 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 api/src/repositories/survey-block-repository.test.ts diff --git a/api/src/repositories/survey-block-repository.test.ts b/api/src/repositories/survey-block-repository.test.ts new file mode 100644 index 0000000000..59e6a1003b --- /dev/null +++ b/api/src/repositories/survey-block-repository.test.ts @@ -0,0 +1,156 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { getMockDBConnection } from '../__mocks__/db'; +import { SurveyBlock, SurveyBlockRepository } from './survey-block-repository'; + +chai.use(sinonChai); + +describe.only('SurveyBlockRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getSurveyBlocksForSurveyId', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: '', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + const response = await repo.getSurveyBlocksForSurveyId(1); + + response.forEach((item) => { + expect(item.survey_id).to.be.eql(1); + }); + }); + + it('should succeed with empty data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + const response = await repo.getSurveyBlocksForSurveyId(1); + expect(response).to.be.empty; + }); + }); + + describe('insertSurveyBlock', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: 'new', + description: 'block', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + const repo = new SurveyBlockRepository(dbConnection); + + const block: SurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' }; + const response = await repo.insertSurveyBlock(block); + + expect(response.name).to.be.eql('new'); + expect(response.description).to.be.eql('block'); + }); + + it('should fail with erroneous data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + const repo = new SurveyBlockRepository(dbConnection); + try { + const block = ({ survey_block_id: null, survey_id: 1, name: null, description: null } as any) as SurveyBlock; + await repo.insertSurveyBlock(block); + expect.fail(); + } catch (error) { + expect(((error as any) as ApiExecuteSQLError).message).to.be.eq('Failed to insert survey block'); + } + }); + }); + + describe('deleteSurveyBlockRecord', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: 'Deleted record', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + const response = await repo.deleteSurveyBlockRecord(1); + expect(response.survey_block_id).to.be.eql(1); + }); + + it('should failed with erroneous data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + try { + await repo.deleteSurveyBlockRecord(1); + expect.fail(); + } catch (error) { + expect(((error as any) as ApiExecuteSQLError).message).to.be.eq('Failed to delete survey block record'); + } + }); + }); +}); diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index 4c7c791aed..b84d71c598 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -61,7 +61,13 @@ export class SurveyBlockRepository extends BaseRepository { */ async updateSurveyBlock(block: SurveyBlock): Promise { const sql = SQL` - UPDATE survey_block SET name = ${block.name}, description = ${block.description}, survey_id=${block.survey_id} WHERE survey_block_id = ${block.survey_block_id}; + UPDATE survey_block + SET + name = ${block.name}, + description = ${block.description}, + survey_id=${block.survey_id} + WHERE + survey_block_id = ${block.survey_block_id}; `; await this.connection.sql(sql); } @@ -90,7 +96,7 @@ export class SurveyBlockRepository extends BaseRepository { const response = await this.connection.sql(sql, SurveyBlockRecord); if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to insert project team member', [ + throw new ApiExecuteSQLError('Failed to insert survey block', [ 'SurveyBlockRepository->postSurveyBlock', 'rows was null or undefined, expected rows != null' ]); From d3d457fc1ff065bca3cce5feb2ee8639e891b11f Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Sep 2023 13:51:54 -0700 Subject: [PATCH 10/20] added test for updating survey block --- .../survey-block-repository.test.ts | 49 +++++++++++++++++++ .../repositories/survey-block-repository.ts | 17 +++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/api/src/repositories/survey-block-repository.test.ts b/api/src/repositories/survey-block-repository.test.ts index 59e6a1003b..fd4923b433 100644 --- a/api/src/repositories/survey-block-repository.test.ts +++ b/api/src/repositories/survey-block-repository.test.ts @@ -59,6 +59,55 @@ describe.only('SurveyBlockRepository', () => { }); }); + describe('updateSurveyBlock', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: 'Updated name', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + const block: SurveyBlock = { survey_block_id: 1, survey_id: 1, name: 'Updated name', description: 'block' }; + const response = await repo.updateSurveyBlock(block); + expect(response.survey_block_id).to.be.eql(1); + expect(response.name).to.be.eql('Updated name'); + }); + + it('should failed with erroneous data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repo = new SurveyBlockRepository(dbConnection); + const block: SurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' }; + try { + await repo.updateSurveyBlock(block); + expect.fail(); + } catch (error) { + expect(((error as any) as ApiExecuteSQLError).message).to.be.eq('Failed to update survey block'); + } + }); + }); + describe('insertSurveyBlock', () => { it('should succeed with valid data', async () => { const mockResponse = ({ diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index b84d71c598..5e61b9d49f 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -59,7 +59,7 @@ export class SurveyBlockRepository extends BaseRepository { * @return {*} {Promise} * @memberof SurveyBlockRepository */ - async updateSurveyBlock(block: SurveyBlock): Promise { + async updateSurveyBlock(block: SurveyBlock): Promise { const sql = SQL` UPDATE survey_block SET @@ -67,9 +67,20 @@ export class SurveyBlockRepository extends BaseRepository { description = ${block.description}, survey_id=${block.survey_id} WHERE - survey_block_id = ${block.survey_block_id}; + survey_block_id = ${block.survey_block_id} + RETURNING + *; `; - await this.connection.sql(sql); + const response = await this.connection.sql(sql, SurveyBlockRecord); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to update survey block', [ + 'SurveyBlockRepository->updateSurveyBlock', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows[0]; } /** From 0275e95beb5ad4a509a379a3f335e8cd196609d2 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Sep 2023 15:12:30 -0700 Subject: [PATCH 11/20] added survey block service --- .../survey-block-repository.test.ts | 2 +- api/src/services/survey-block-service.test.ts | 196 ++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 api/src/services/survey-block-service.test.ts diff --git a/api/src/repositories/survey-block-repository.test.ts b/api/src/repositories/survey-block-repository.test.ts index fd4923b433..5f52b69dec 100644 --- a/api/src/repositories/survey-block-repository.test.ts +++ b/api/src/repositories/survey-block-repository.test.ts @@ -9,7 +9,7 @@ import { SurveyBlock, SurveyBlockRepository } from './survey-block-repository'; chai.use(sinonChai); -describe.only('SurveyBlockRepository', () => { +describe('SurveyBlockRepository', () => { afterEach(() => { sinon.restore(); }); diff --git a/api/src/services/survey-block-service.test.ts b/api/src/services/survey-block-service.test.ts new file mode 100644 index 0000000000..5cc6599018 --- /dev/null +++ b/api/src/services/survey-block-service.test.ts @@ -0,0 +1,196 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { SurveyBlock, SurveyBlockRepository } from '../repositories/survey-block-repository'; +import { getMockDBConnection } from '../__mocks__/db'; +import { SurveyBlockService } from './survey-block-service'; + +chai.use(sinonChai); + +describe('SurveyBlockService', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getSurveyBlocksForSurveyId', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: '', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const service = new SurveyBlockService(dbConnection); + const response = await service.getSurveyBlocksForSurveyId(1); + + response.forEach((item) => { + expect(item.survey_id).to.be.eql(1); + }); + }); + + it('should succeed with empty data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const service = new SurveyBlockService(dbConnection); + const response = await service.getSurveyBlocksForSurveyId(1); + expect(response).to.be.empty; + }); + }); + + describe('updateInsertSurveyBlocks', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: 'Updated name', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + const service = new SurveyBlockService(dbConnection); + + const update = sinon.stub(SurveyBlockRepository.prototype, 'updateSurveyBlock').resolves(); + const insert = sinon.stub(SurveyBlockRepository.prototype, 'insertSurveyBlock').resolves(); + const blocks: SurveyBlock[] = [ + { survey_block_id: 1, survey_id: 1, name: 'Old Block', description: 'Updated' }, + { survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' } + ]; + + await service.updateInsertSurveyBlocks(1, blocks); + expect(update).to.be.calledOnce; + expect(insert).to.be.calledOnce; + }); + }); + + describe('upsertSurveyBlocks', () => { + it('should succeed with valid data', async () => { + const dbConnection = getMockDBConnection(); + const service = new SurveyBlockService(dbConnection); + + const getOldBlocks = sinon.stub(SurveyBlockService.prototype, 'getSurveyBlocksForSurveyId').resolves([]); + const deleteBlock = sinon.stub(SurveyBlockService.prototype, 'deleteSurveyBlock').resolves(); + const updateInsert = sinon.stub(SurveyBlockService.prototype, 'updateInsertSurveyBlocks').resolves(); + + const blocks: SurveyBlock[] = [ + { survey_block_id: null, survey_id: 1, name: 'Old Block', description: 'Updated' }, + { survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' } + ]; + await service.upsertSurveyBlocks(1, blocks); + + expect(getOldBlocks).to.be.calledOnce; + expect(updateInsert).to.be.calledOnce; + expect(deleteBlock).to.not.be.calledOnce; + }); + + it('should run delete block code', async () => { + const dbConnection = getMockDBConnection(); + const service = new SurveyBlockService(dbConnection); + + const getOldBlocks = sinon.stub(SurveyBlockService.prototype, 'getSurveyBlocksForSurveyId').resolves([ + { + survey_block_id: 10, + survey_id: 1, + name: 'Old Block', + description: 'Updated', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ]); + const deleteBlock = sinon.stub(SurveyBlockService.prototype, 'deleteSurveyBlock').resolves(); + const updateInsert = sinon.stub(SurveyBlockService.prototype, 'updateInsertSurveyBlocks').resolves(); + + const blocks: SurveyBlock[] = [ + { survey_block_id: null, survey_id: 1, name: 'Old Block', description: 'Updated' }, + { survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' } + ]; + await service.upsertSurveyBlocks(1, blocks); + + expect(getOldBlocks).to.be.calledOnce; + expect(updateInsert).to.be.calledOnce; + expect(deleteBlock).to.be.calledOnce; + }); + }); + + describe('deleteSurveyBlockRecord', () => { + it('should succeed with valid data', async () => { + const mockResponse = ({ + rows: [ + { + survey_block_id: 1, + survey_id: 1, + name: 'Deleted record', + description: '', + create_date: '', + create_user: 1, + update_date: '', + update_user: 1, + revision_count: 1 + } + ], + rowCount: 1 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const service = new SurveyBlockService(dbConnection); + const response = await service.deleteSurveyBlock(1); + expect(response.survey_block_id).to.be.eql(1); + }); + + it('should failed with erroneous data', async () => { + const mockResponse = ({ + rows: [], + rowCount: 0 + } as any) as Promise>; + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const service = new SurveyBlockService(dbConnection); + try { + await service.deleteSurveyBlock(1); + expect.fail(); + } catch (error) { + expect(((error as any) as ApiExecuteSQLError).message).to.be.eq('Failed to delete survey block record'); + } + }); + }); +}); From f1b489de4cecc03a78ca99a8e17c764f74307ea7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 6 Sep 2023 09:53:24 -0700 Subject: [PATCH 12/20] added snackbar components to survey block dialogs --- .../components/CreateSurveyBlockDialog.tsx | 39 +++++++++++++- .../components/EditSurveyBlockDialog.tsx | 52 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx b/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx index 875a6172ce..6ac8f4d511 100644 --- a/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx +++ b/app/src/features/surveys/components/CreateSurveyBlockDialog.tsx @@ -1,7 +1,11 @@ +import CloseIcon from '@mui/icons-material/Close'; +import { Typography } from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import Snackbar from '@mui/material/Snackbar'; import EditDialog from 'components/dialog/EditDialog'; +import { useState } from 'react'; import BlockForm from './BlockForm'; import { BlockYupSchema } from './SurveyBlockSection'; - interface ICreateBlockProps { open: boolean; onSave: (data: any) => void; @@ -10,6 +14,8 @@ interface ICreateBlockProps { const CreateSurveyBlockDialog: React.FC = (props) => { const { open, onSave, onClose } = props; + const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); + const [blockName, setBlockName] = useState(''); return ( <> = (props) => { }} dialogSaveButtonLabel="Add Block" onCancel={() => onClose()} - onSave={(formValues) => onSave(formValues)} + onSave={(formValues) => { + setBlockName(formValues.name); + setIsSnackBarOpen(true); + onSave(formValues); + }} + /> + {/* This is done instead of the dialogContext because that causes the form to rerender, removing any changes the user makes */} + { + setIsSnackBarOpen(false); + setBlockName(''); + }} + message={ + <> + + Block {blockName} has been added. + + + } + action={ + setIsSnackBarOpen(false)}> + + + } /> ); diff --git a/app/src/features/surveys/components/EditSurveyBlockDialog.tsx b/app/src/features/surveys/components/EditSurveyBlockDialog.tsx index 20049585fa..8af6ac4001 100644 --- a/app/src/features/surveys/components/EditSurveyBlockDialog.tsx +++ b/app/src/features/surveys/components/EditSurveyBlockDialog.tsx @@ -1,4 +1,9 @@ +import CloseIcon from '@mui/icons-material/Close'; +import { Typography } from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import Snackbar from '@mui/material/Snackbar'; import EditDialog from 'components/dialog/EditDialog'; +import { useState } from 'react'; import BlockForm from './BlockForm'; import { BlockYupSchema, IEditBlock } from './SurveyBlockSection'; @@ -11,6 +16,8 @@ interface IEditBlockProps { const EditSurveyBlockDialog: React.FC = (props) => { const { open, initialData, onSave, onClose } = props; + const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); + const [blockName, setBlockName] = useState(''); return ( <> = (props) => { validationSchema: BlockYupSchema }} dialogSaveButtonLabel="Save" - onCancel={() => onClose()} - onSave={(formValues) => onSave(formValues, initialData?.index)} + onCancel={() => { + setBlockName(''); + setIsSnackBarOpen(true); + onClose(); + }} + onSave={(formValues) => { + setBlockName(formValues.name); + setIsSnackBarOpen(true); + onSave(formValues, initialData?.index); + }} + /> + {/* This is done instead of the dialogContext because that causes the form to rerender, removing any changes the user makes */} + { + setIsSnackBarOpen(false); + setBlockName(''); + }} + message={ + <> + + {initialData?.block.survey_block_id ? ( + <> + Block {blockName} has been updated. + + ) : ( + <> + Block {blockName} has been added. + + )} + + + } + action={ + setIsSnackBarOpen(false)}> + + + } /> ); From 2b98edd239e2b727cd363e446ca0cab5dfb879ac Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 6 Sep 2023 09:55:00 -0700 Subject: [PATCH 13/20] fixed warning on anchor --- app/src/features/surveys/components/SurveyBlockSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/components/SurveyBlockSection.tsx b/app/src/features/surveys/components/SurveyBlockSection.tsx index 8cc41d44f9..61d7cd2daa 100644 --- a/app/src/features/surveys/components/SurveyBlockSection.tsx +++ b/app/src/features/surveys/components/SurveyBlockSection.tsx @@ -85,10 +85,10 @@ const SurveyBlockSection: React.FC = (props) => { setAnchorEl(null); }} onSave={(data, index) => { - setEditData(undefined); - setFieldValue(`${name}[${index}]`, data); setIsEditModalOpen(false); setAnchorEl(null); + setEditData(undefined); + setFieldValue(`${name}[${index}]`, data); }} /> Date: Wed, 6 Sep 2023 10:00:13 -0700 Subject: [PATCH 14/20] fixed code smell --- api/src/repositories/survey-block-repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/repositories/survey-block-repository.ts b/api/src/repositories/survey-block-repository.ts index 5e61b9d49f..dc9265d32d 100644 --- a/api/src/repositories/survey-block-repository.ts +++ b/api/src/repositories/survey-block-repository.ts @@ -135,7 +135,7 @@ export class SurveyBlockRepository extends BaseRepository { const response = await this.connection.sql(sqlStatement, SurveyBlockRecord); - if (!response || !response.rowCount) { + if (!response?.rowCount) { throw new ApiExecuteSQLError('Failed to delete survey block record', [ 'SurveyBlockRepository->deleteSurveyBlockRecord', 'rows was null or undefined, expected rows != null' From c58a3eb9fcea175883b85c099518ff6b591e33aa Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 6 Sep 2023 10:43:44 -0700 Subject: [PATCH 15/20] fixed more tests --- api/src/services/survey-service.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/src/services/survey-service.test.ts b/api/src/services/survey-service.test.ts index 6c9c64ec1c..b0d1683538 100644 --- a/api/src/services/survey-service.test.ts +++ b/api/src/services/survey-service.test.ts @@ -33,6 +33,7 @@ import { getMockDBConnection } from '../__mocks__/db'; import { HistoryPublishService } from './history-publish-service'; import { PermitService } from './permit-service'; import { PlatformService } from './platform-service'; +import { SurveyBlockService } from './survey-block-service'; import { SurveyParticipationService } from './survey-participation-service'; import { SurveyService } from './survey-service'; import { TaxonomyService } from './taxonomy-service'; @@ -78,6 +79,7 @@ describe('SurveyService', () => { const getSurveyParticipantsStub = sinon .stub(SurveyParticipationService.prototype, 'getSurveyParticipants') .resolves([{ data: 'participantData' } as any]); + const getSurveyBlockStub = sinon.stub(SurveyBlockService.prototype, 'getSurveyBlocksForSurveyId').resolves([]); const getSurveyPartnershipsDataStub = sinon.stub(SurveyService.prototype, 'getSurveyPartnershipsData').resolves({ indigenous_partnerships: [], @@ -95,6 +97,7 @@ describe('SurveyService', () => { expect(getSurveyLocationDataStub).to.be.calledOnce; expect(getSurveyParticipantsStub).to.be.calledOnce; expect(getSurveyPartnershipsDataStub).to.be.calledOnce; + expect(getSurveyBlockStub).to.be.calledOnce; expect(response).to.eql({ survey_details: { data: 'surveyData' }, @@ -108,7 +111,8 @@ describe('SurveyService', () => { stakeholder_partnerships: [] }, participants: [{ data: 'participantData' } as any], - location: { data: 'locationData' } + location: { data: 'locationData' }, + blocks: [] }); }); }); @@ -137,6 +141,7 @@ describe('SurveyService', () => { const upsertSurveyParticipantDataStub = sinon .stub(SurveyService.prototype, 'upsertSurveyParticipantData') .resolves(); + sinon.stub(SurveyBlockService.prototype, 'upsertSurveyBlocks').resolves(); const surveyService = new SurveyService(dbConnectionObj); @@ -175,6 +180,7 @@ describe('SurveyService', () => { const upsertSurveyParticipantDataStub = sinon .stub(SurveyService.prototype, 'upsertSurveyParticipantData') .resolves(); + const upsertBlocks = sinon.stub(SurveyBlockService.prototype, 'upsertSurveyBlocks').resolves(); const surveyService = new SurveyService(dbConnectionObj); @@ -187,7 +193,8 @@ describe('SurveyService', () => { proprietor: {}, purpose_and_methodology: {}, location: {}, - participants: [{}] + participants: [{}], + blocks: [{}] }); await surveyService.updateSurvey(surveyId, putSurveyData); @@ -201,6 +208,7 @@ describe('SurveyService', () => { expect(updateSurveyProprietorDataStub).to.have.been.calledOnce; expect(updateSurveyRegionStub).to.have.been.calledOnce; expect(upsertSurveyParticipantDataStub).to.have.been.calledOnce; + expect(upsertBlocks).to.have.been.calledOnce; }); }); From d3552dbf4af3ecd96779e1f0fed066a8c0c63e2b Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 7 Sep 2023 10:51:22 -0700 Subject: [PATCH 16/20] updated card UI, fixed issue with click, added comments and simplified repo code --- api/src/services/survey-block-service.ts | 32 +++--------- api/src/services/survey-service.ts | 8 +++ app/src/features/surveys/CreateSurveyPage.tsx | 2 +- .../surveys/components/SurveyBlockSection.tsx | 49 ++++++++++++------- .../features/surveys/edit/EditSurveyForm.tsx | 2 +- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/api/src/services/survey-block-service.ts b/api/src/services/survey-block-service.ts index 67852c8db9..4e9fa93c03 100644 --- a/api/src/services/survey-block-service.ts +++ b/api/src/services/survey-block-service.ts @@ -32,29 +32,6 @@ export class SurveyBlockService extends DBService { return this.surveyBlockRepository.deleteSurveyBlockRecord(surveyBlockId); } - /** - * Insert or Updates Survey Block Records based on the existence of a survey_block_id - * - * @param {number} surveyId - * @param {SurveyBlock[]} blocks - * @return {*} {Promise} - * @memberof SurveyBlockService - */ - async updateInsertSurveyBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { - const insertUpdate: Promise[] = []; - - blocks.forEach((item: SurveyBlock) => { - item.survey_id = surveyId; - if (item.survey_block_id) { - insertUpdate.push(this.surveyBlockRepository.updateSurveyBlock(item)); - } else { - insertUpdate.push(this.surveyBlockRepository.insertSurveyBlock(item)); - } - }); - - await Promise.all(insertUpdate); - } - /** * Inserts, Updates and Deletes Block records * All passed in blocks are treated as the source of truth, @@ -82,7 +59,14 @@ export class SurveyBlockService extends DBService { }); // update or insert block data - promises.push(this.updateInsertSurveyBlocks(surveyId, blocks)); + blocks.forEach((item: SurveyBlock) => { + item.survey_id = surveyId; + if (item.survey_block_id) { + promises.push(this.surveyBlockRepository.updateSurveyBlock(item)); + } else { + promises.push(this.surveyBlockRepository.insertSurveyBlock(item)); + } + }); await Promise.all(promises); } diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 2fed4e459e..c40d73860a 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -462,6 +462,14 @@ export class SurveyService extends DBService { return surveyId; } + /** + * Insert, updates and deletes Survey Blocks for a given survey id + * + * @param {number} surveyId + * @param {SurveyBlock[]} blocks + * @returns {*} {Promise} + * @memberof SurveyService + */ async upsertBlocks(surveyId: number, blocks: SurveyBlock[]): Promise { const service = new SurveyBlockService(this.connection); return service.upsertSurveyBlocks(surveyId, blocks); diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index a45ec09068..4d65d2a87c 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -382,7 +382,7 @@ const CreateSurveyPage = () => { } + component={} /> diff --git a/app/src/features/surveys/components/SurveyBlockSection.tsx b/app/src/features/surveys/components/SurveyBlockSection.tsx index 61d7cd2daa..cfb194e0b9 100644 --- a/app/src/features/surveys/components/SurveyBlockSection.tsx +++ b/app/src/features/surveys/components/SurveyBlockSection.tsx @@ -1,7 +1,7 @@ import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import { ListItemIcon, Menu, MenuItem, MenuProps } from '@mui/material'; +import { ListItemIcon, Menu, MenuItem, MenuProps, Typography } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -14,18 +14,14 @@ import yup from 'utils/YupSchema'; import CreateSurveyBlockDialog from './CreateSurveyBlockDialog'; import EditSurveyBlockDialog from './EditSurveyBlockDialog'; -interface ISurveyBlockFormProps { - name: string; // the name of the formik field value -} - export const SurveyBlockInitialValues = { blocks: [] }; // Form validation for Block Item export const BlockYupSchema = yup.object({ - name: yup.string().required(), - description: yup.string().required() + name: yup.string().required().max(50, 'Maximum 50 characters'), + description: yup.string().required().max(250, 'Maximum 250 characters') }); export const SurveyBlockYupSchema = yup.array(BlockYupSchema); @@ -39,8 +35,7 @@ export interface IEditBlock { }; } -const SurveyBlockSection: React.FC = (props) => { - const { name } = props; +const SurveyBlockSection: React.FC = () => { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); @@ -49,7 +44,7 @@ const SurveyBlockSection: React.FC = (props) => { const formikProps = useFormikContext(); const { values, handleSubmit, setFieldValue } = formikProps; - const handleMenuClick = (event: React.MouseEvent, index: number) => { + const handleMenuClick = (event: React.MouseEvent, index: number) => { setAnchorEl(event.currentTarget); setEditData({ index: index, block: values.blocks[index] }); }; @@ -58,7 +53,7 @@ const SurveyBlockSection: React.FC = (props) => { if (editData) { const data = values.blocks; data.splice(editData.index, 1); - setFieldValue(name, data); + setFieldValue('blocks', data); } setAnchorEl(null); }; @@ -71,7 +66,7 @@ const SurveyBlockSection: React.FC = (props) => { onClose={() => setIsCreateModalOpen(false)} onSave={(data) => { setEditData(undefined); - setFieldValue(`${name}[${values.blocks.length}]`, data); + setFieldValue(`blocks[${values.blocks.length}]`, data); setIsCreateModalOpen(false); }} /> @@ -88,9 +83,26 @@ const SurveyBlockSection: React.FC = (props) => { setIsEditModalOpen(false); setAnchorEl(null); setEditData(undefined); - setFieldValue(`${name}[${index}]`, data); + setFieldValue(`blocks[${index}]`, data); }} /> + + Define Blocks + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor sem. Aliquam erat volutpat. Donec + placerat nisl magna, et faucibus arcu condimentum sed. + setAnchorEl(null)} @@ -119,7 +131,7 @@ const SurveyBlockSection: React.FC = (props) => {