From 54ec3f1e50dbbcc20c938779aba70d2e772ea640 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Thu, 11 Jul 2024 08:57:21 +1200 Subject: [PATCH 01/17] tweak(adminPanel): RN-1253: Display entity code in entity question answers (#5655) Co-authored-by: Andrew --- .../admin-panel/src/routes/surveys/surveyResponses.js | 9 +++++++++ .../src/apiV2/answers/assertAnswerPermissions.js | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/admin-panel/src/routes/surveys/surveyResponses.js b/packages/admin-panel/src/routes/surveys/surveyResponses.js index 6c84fae093..637a5d446a 100644 --- a/packages/admin-panel/src/routes/surveys/surveyResponses.js +++ b/packages/admin-panel/src/routes/surveys/surveyResponses.js @@ -120,6 +120,15 @@ export const ANSWER_COLUMNS = [ { Header: 'Answer', source: 'text', + type: 'tooltip', + accessor: row => { + return row['entity.code'] || row.text; + }, + }, + { + Header: 'EntityName', + show: false, + source: 'entity.code', }, ]; diff --git a/packages/central-server/src/apiV2/answers/assertAnswerPermissions.js b/packages/central-server/src/apiV2/answers/assertAnswerPermissions.js index 20919c23c7..5b3d1420e8 100644 --- a/packages/central-server/src/apiV2/answers/assertAnswerPermissions.js +++ b/packages/central-server/src/apiV2/answers/assertAnswerPermissions.js @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd */ -import { QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database'; +import { JOIN_TYPES, QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database'; import { hasBESAdminAccess } from '../../permissions'; import { fetchCountryCodesByPermissionGroupId, mergeMultiJoin } from '../utilities'; import { assertSurveyResponsePermissions } from '../surveyResponses'; @@ -102,6 +102,11 @@ export const createAnswerViaSurveyResponseDBFilter = async ( joinWith: RECORDS.SURVEY_RESPONSE, joinCondition: [`${RECORDS.SURVEY_RESPONSE}.id`, `${RECORDS.ANSWER}.survey_response_id`], }, + { + joinWith: RECORDS.ENTITY, + joinCondition: [`${RECORDS.ENTITY}.id`, `${RECORDS.ANSWER}.text`], + joinType: JOIN_TYPES.LEFT, + }, { joinWith: RECORDS.SURVEY_SCREEN, joinCondition: [ From c398d9983f7d9169a94e80f20cfad0d8ec02b94c Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:36:06 +1200 Subject: [PATCH 02/17] fix(adminPanel): NOTUP-722: Refresh data on admin panel on successful delete (#5774) --- .../src/table/DataFetchingTable/DataFetchingTable.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx index 016a4b2e28..059c6fb0ac 100644 --- a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx +++ b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx @@ -174,6 +174,14 @@ const DataFetchingTableComponent = memo( onRefreshData(filters, sorting, pageIndex, pageSize); }, [pageSize, pageIndex]); + // when a delete is successful, the confirmActionMessage will be set to null. Refresh the data here + useEffect(() => { + // Don't refresh data if there is a confirmActionMessage or errorMessage, or if data is being changed on the server + if (confirmActionMessage || errorMessage || isChangingDataOnServer) return; + + onRefreshData(filters, sorting, pageIndex, pageSize); + }, [confirmActionMessage, errorMessage, isChangingDataOnServer]); + useEffect(() => { if (editorState?.isOpen) return; onRefreshData(filters, sorting, pageIndex, pageSize); From 27f8364288b242e0f5669280074abe299d8ec732 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:55:31 +1200 Subject: [PATCH 03/17] tweak(security): RN-1382: Send export links to email instead of attachments (#5780) Default to download links for exports --- .../apiV2/export/{download => }/DownloadHandler.js | 6 +++--- .../src/apiV2/export/download/createDownloadLink.js | 11 ----------- .../central-server/src/apiV2/export/download/index.js | 2 -- packages/central-server/src/apiV2/export/index.js | 2 +- packages/server-utils/src/constructExportEmail.ts | 10 ++++++---- 5 files changed, 10 insertions(+), 21 deletions(-) rename packages/central-server/src/apiV2/export/{download => }/DownloadHandler.js (81%) delete mode 100644 packages/central-server/src/apiV2/export/download/createDownloadLink.js delete mode 100644 packages/central-server/src/apiV2/export/download/index.js diff --git a/packages/central-server/src/apiV2/export/download/DownloadHandler.js b/packages/central-server/src/apiV2/export/DownloadHandler.js similarity index 81% rename from packages/central-server/src/apiV2/export/download/DownloadHandler.js rename to packages/central-server/src/apiV2/export/DownloadHandler.js index a979569fc7..8da3f0e86e 100644 --- a/packages/central-server/src/apiV2/export/download/DownloadHandler.js +++ b/packages/central-server/src/apiV2/export/DownloadHandler.js @@ -4,9 +4,9 @@ */ import fs from 'fs'; import { respondWithDownload, ValidationError } from '@tupaia/utils'; -import { RouteHandler } from '../../RouteHandler'; -import { assertAdminPanelAccess } from '../../../permissions'; -import { getExportPathForUser } from '../getExportPathForUser'; +import { getExportPathForUser } from './getExportPathForUser'; +import { RouteHandler } from '../RouteHandler'; +import { assertAdminPanelAccess } from '../../permissions'; export class DownloadHandler extends RouteHandler { async assertUserHasAccess() { diff --git a/packages/central-server/src/apiV2/export/download/createDownloadLink.js b/packages/central-server/src/apiV2/export/download/createDownloadLink.js deleted file mode 100644 index f9f9c99b63..0000000000 --- a/packages/central-server/src/apiV2/export/download/createDownloadLink.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd - */ - -import path from 'path'; - -export const createDownloadLink = filePath => { - const fileName = path.basename(filePath); - return `${process.env.ADMIN_PANEL_SERVER_URL}/v1/export/download/${encodeURIComponent(fileName)}`; -}; diff --git a/packages/central-server/src/apiV2/export/download/index.js b/packages/central-server/src/apiV2/export/download/index.js deleted file mode 100644 index 71e4bb35a8..0000000000 --- a/packages/central-server/src/apiV2/export/download/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { createDownloadLink } from './createDownloadLink'; -export { DownloadHandler } from './DownloadHandler'; diff --git a/packages/central-server/src/apiV2/export/index.js b/packages/central-server/src/apiV2/export/index.js index 646fcb9601..9d1177711f 100644 --- a/packages/central-server/src/apiV2/export/index.js +++ b/packages/central-server/src/apiV2/export/index.js @@ -8,7 +8,7 @@ import { emailAfterTimeout } from '@tupaia/server-boilerplate'; import { constructExportEmail } from '@tupaia/server-utils'; import { catchAsyncErrors } from '../middleware'; import { useRouteHandler } from '../RouteHandler'; -import { DownloadHandler } from './download'; +import { DownloadHandler } from './DownloadHandler'; import { exportOptionSet } from './exportOptionSet'; import { exportSurveyResponses } from './exportSurveyResponses'; import { exportSurveys } from './exportSurveys'; diff --git a/packages/server-utils/src/constructExportEmail.ts b/packages/server-utils/src/constructExportEmail.ts index 811ce5cbf3..2733c81510 100644 --- a/packages/server-utils/src/constructExportEmail.ts +++ b/packages/server-utils/src/constructExportEmail.ts @@ -26,12 +26,14 @@ type ResponseBody = { filePath?: string; }; -type ReqBody = { - emailExportFileMode?: EmailExportFileModes; +type Req = { + query: { + emailExportFileMode?: EmailExportFileModes; + }; }; -export const constructExportEmail = async (responseBody: ResponseBody, reqBody: ReqBody) => { - const { emailExportFileMode = EmailExportFileModes.ATTACHMENT } = reqBody; +export const constructExportEmail = async (responseBody: ResponseBody, req: Req) => { + const { emailExportFileMode = EmailExportFileModes.DOWNLOAD_LINK } = req.query; const { error, filePath } = responseBody; const subject = 'Your export from Tupaia'; if (error) { From de888fd95a09f8b01e20f7c747335915966e634c Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:56:07 +1200 Subject: [PATCH 04/17] fix(adminPanel): RN-1235: Handle empty string sort order when creating and editing projects (#5709) * fix(adminPanel): RN-1235: Handle empty string sort order when creating projects * Update EditProject.test.js * Fix empty sort order string * Set empty string to be null on edit --------- Co-authored-by: Andrew --- packages/admin-panel/src/editor/actions.js | 2 +- .../src/apiV2/projects/CreateProject.js | 4 ++-- .../src/apiV2/projects/EditProject.js | 5 +++++ .../tests/apiV2/projects/EditProject.test.js | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/admin-panel/src/editor/actions.js b/packages/admin-panel/src/editor/actions.js index 4731b5e165..2fbf684cb1 100644 --- a/packages/admin-panel/src/editor/actions.js +++ b/packages/admin-panel/src/editor/actions.js @@ -195,7 +195,7 @@ export const editField = (fieldSource, newValue) => (dispatch, getState) => { dispatch({ type: EDITOR_FIELD_EDIT, fieldKey: fieldSourceToEdit, - newValue, + newValue: newValue === '' ? null : newValue, }); }; diff --git a/packages/central-server/src/apiV2/projects/CreateProject.js b/packages/central-server/src/apiV2/projects/CreateProject.js index f3949c8325..ffb621a99b 100644 --- a/packages/central-server/src/apiV2/projects/CreateProject.js +++ b/packages/central-server/src/apiV2/projects/CreateProject.js @@ -33,7 +33,7 @@ export class CreateProject extends BESAdminCreateHandler { code: rawProjectCode, name, description, - sort_order: sortOrder, + sort_order: sortOrder = null, permission_groups: permissionGroups, countries, entityTypes, @@ -74,7 +74,7 @@ export class CreateProject extends BESAdminCreateHandler { const newProject = await transactingModels.project.create({ code: projectCode, description, - sort_order: sortOrder, + sort_order: sortOrder === '' ? null : sortOrder, image_url: '', logo_url: '', permission_groups: [projectPermissionGroup.name, ...permissionGroups], diff --git a/packages/central-server/src/apiV2/projects/EditProject.js b/packages/central-server/src/apiV2/projects/EditProject.js index 10e39a2319..8217399478 100644 --- a/packages/central-server/src/apiV2/projects/EditProject.js +++ b/packages/central-server/src/apiV2/projects/EditProject.js @@ -47,6 +47,7 @@ export class EditProject extends EditHandler { image_url: encodedBackgroundImage, logo_url: encodedLogoImage, code: updatedCode, + sort_order: updatedSortOrder, } = this.updatedFields; const updatedFields = { ...this.updatedFields }; @@ -76,6 +77,10 @@ export class EditProject extends EditHandler { existingLogoImage, ); } + // If the sort_order is an empty string, we want to set the sort_order to null, as the sort_order field is an integer and an empty string will cause an error + if (updatedSortOrder === '') { + updatedFields.sort_order = null; + } await this.models.project.updateById(this.recordId, updatedFields); } } diff --git a/packages/central-server/src/tests/apiV2/projects/EditProject.test.js b/packages/central-server/src/tests/apiV2/projects/EditProject.test.js index d5f5d97076..e46d1bda03 100644 --- a/packages/central-server/src/tests/apiV2/projects/EditProject.test.js +++ b/packages/central-server/src/tests/apiV2/projects/EditProject.test.js @@ -35,6 +35,7 @@ describe('Editing a project', async () => { logo_url: 'www.image.com', description: 'old description', code: 'test_existing_project', + sort_order: 1, }; const ENCODED_IMAGE = 'data:image/gif;base64,R0lGODdh4AHwANUAAKqqqgAAAO7u7ru7u+Xl5czMzN3d3cPDw7KystTU1H9/fxcXF1VVVXJych0dHRkZGS4uLo+Pjzc3N5SUlMHBwSwsLGpqahUVFSoqKklJScjIyBgYGLm5uTk5OW9vbzAwMKurqxYWFqWlpaOjoxwcHEJCQp+fn3d3d0ZGRhoaGpubm4WFhV1dXVlZWT8/P4qKimFhYTMzM25ubtDQ0ExMTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA4AHwAAAG/kCAcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAwocSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOn/s+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsyYmIIAQyxcCIBBAREFky9YmKIAQ4AADJA8HoI5gObGlhZABmAhgwYBFCBsBjBhAQgBIAJYjoJCxowZNGYXUS2k9u3cu1FHYtDAgZANHBAAQEBhg5ASDQYAGHAChZQEBRAgSGC9CHPnALBr5+5dOSQFDwqs7uBhiIcOQlJIF4IghREXySngQhITkFAEfPLlt990/rn3CAQRALBadQxMwMAHFAgRwIIq/qxGRAIQTEAbBAkk4UFoREAooYYceujgIgxIcMCK04HwQAgBiLCfiwi4OIQIFUxQQYZJQEdEjDOuxqOPLx4ywQNErqbAAhoQUNtsAYg4BJPbvRDACwsawQADCz4ZpYZaatjkIhV85uZntkl3AAjlObDCEBOgZwQMLWSQBHwcENHmm5/Ziaeeax5igACMChAAowGMMMQIBgLAAAtDtIDigTEkYEFyRBgX5qKNPirApZlumugiqwkp4gQlgCCECboBAKgRtWVYQIhGVBCBdkmsRqtlt67KyGoHiIBjCCYAO0BtAcRpRAkR7CdCCUV49iYSqz2rmrTGLiLAEAMQIAABwMAKcYC56B6RwIxCDFAiEaQ2isS46rKbbrj89uvvvwAHLPDABBds8MEIJ6zwwgw37PDDEEcs8cQUV2zxxRhnrPHGHHfs8ccghyzyyCSXbPLJKKes8sost+zyyzDHLPPMNNds880456zzzjz37PPPQAct9NBEF2300UgnrfTSTDft9NNQRy311FRXbfXVWGet9dZcd+3112CHLfbYZJdt9tlop6322my37fbbcMct99x012333XjnrffefPft98pBAAA7'; @@ -77,6 +78,23 @@ describe('Editing a project', async () => { expect(result[0].description).to.equal('the updated description'); }); + it('updates sort_order to be null if an empty string is passed in', async () => { + await app.grantAccess(BES_ADMIN_POLICY); + + await app.put(`projects/${TEST_PROJECT_INPUT.id}`, { + body: { + ...TEST_PROJECT_INPUT, + sort_order: '', + }, + }); + + const result = await models.project.find({ + id: TEST_PROJECT_INPUT.id, + }); + expect(result.length).to.equal(1); + expect(result[0].sort_order).to.equal(null); + }); + it('uploads the value of image_url if it has changed', async () => { await app.grantAccess(BES_ADMIN_POLICY); From ed7ac7babd52a220017e95e7101560c767bdb5ef Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:17:48 +1200 Subject: [PATCH 05/17] fix(adminPanel): RN-1380: Use default sorting where applicable (#5775) * fix(adminPanel): RN-1380: Use default sorting * Add proptypes --- .../DataFetchingTable/DataFetchingTable.jsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx index 059c6fb0ac..a5b868645b 100644 --- a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx +++ b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx @@ -109,6 +109,7 @@ const DataFetchingTableComponent = memo( actionLabel, defaultFilters, editorState, + defaultSorting, }) => { const formattedColumns = useMemo(() => { const cols = columns.map(column => formatColumnForReactTable(column)); @@ -159,19 +160,27 @@ const DataFetchingTableComponent = memo( return [...nonButtonColumns, singleButtonColumn]; }, [JSON.stringify(columns)]); + const getSortingToUse = () => { + // If there is no sorting, return the default sorting, if it exists, otherwise return an empty array + if (!sorting || sorting.length === 0) return defaultSorting || []; + return sorting; + }; + + const sortingToUse = getSortingToUse(); + // Listen for changes in filters in the URL and refresh the data accordingly const { filters, onChangeFilters } = useColumnFilters(defaultFilters); useEffect(() => { // if the page index is already 0, we can just refresh the data if (pageIndex === 0) { - onRefreshData(filters, sorting, pageIndex, pageSize); + onRefreshData(filters, sortingToUse, pageIndex, pageSize); // if the page index is not 0, we need to reset it to 0, which will trigger a refresh } else onPageChange(0); - }, [JSON.stringify(filters), JSON.stringify(sorting)]); + }, [JSON.stringify(filters), JSON.stringify(sortingToUse)]); useEffect(() => { - onRefreshData(filters, sorting, pageIndex, pageSize); + onRefreshData(filters, sortingToUse, pageIndex, pageSize); }, [pageSize, pageIndex]); // when a delete is successful, the confirmActionMessage will be set to null. Refresh the data here @@ -179,12 +188,12 @@ const DataFetchingTableComponent = memo( // Don't refresh data if there is a confirmActionMessage or errorMessage, or if data is being changed on the server if (confirmActionMessage || errorMessage || isChangingDataOnServer) return; - onRefreshData(filters, sorting, pageIndex, pageSize); + onRefreshData(filters, sortingToUse, pageIndex, pageSize); }, [confirmActionMessage, errorMessage, isChangingDataOnServer]); useEffect(() => { if (editorState?.isOpen) return; - onRefreshData(filters, sorting, pageIndex, pageSize); + onRefreshData(filters, sortingToUse, pageIndex, pageSize); }, [editorState?.isOpen]); const isLoading = isFetchingData || isChangingDataOnServer; @@ -211,7 +220,7 @@ const DataFetchingTableComponent = memo( isLoading={isChangingDataOnServer} pageIndex={pageIndex} pageSize={pageSize} - sorting={sorting} + sorting={sortingToUse} numberOfPages={numberOfPages} onChangeFilters={onChangeFilters} filters={filters} @@ -271,6 +280,7 @@ DataFetchingTableComponent.propTypes = { actionLabel: PropTypes.string, defaultFilters: PropTypes.array, editorState: PropTypes.object, + defaultSorting: PropTypes.array, }; DataFetchingTableComponent.defaultProps = { @@ -288,6 +298,7 @@ DataFetchingTableComponent.defaultProps = { actionLabel: 'Action', defaultFilters: [], editorState: {}, + defaultSorting: [], }; const mapStateToProps = (state, { reduxId, ...ownProps }) => ({ From db1acef3e68d93c96e464383cbb082a3845dd679 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:18:11 +1200 Subject: [PATCH 06/17] feat(adminPanel): RN-1189: Inline mandatory field validation (#5649) * WIP * WIP * Styling * Connected editor * WIP * WIP * WIP * Add buttons * Working edit view * Required styling * Update EditSurveyPage.jsx * Change loading method * File input styling * Reset editor state on close * Required styling * Fix build error * Add tests * Update EditSurveyPage.jsx * Update packages/admin-panel/src/editor/FieldsEditor.jsx Co-authored-by: Jasper Lai <33956381+jaskfla@users.noreply.github.com> * Update packages/admin-panel/src/importExport/ImportModal.jsx Co-authored-by: Jasper Lai <33956381+jaskfla@users.noreply.github.com> * PR fixes * reset file value on clear edits * PR fixes * Sentence casing * Survey question format * Validate fields when editing and creating a survey * Scroll to error on error message appearing * Add aria-hidden for when there is no message to show * WIP * Testing fixes * WIP * Remove ability to clear edits * Styling * Styling * Error handling and data table error handling * WIP * Merge fixes * WIP * Working * WIP * Fix data tables editor not opening * Add comments and scroll * Fix fields * Fix build * Use reusable string * FIx checkbox tooltip styles * Fix edit landing page issue * Fix legend * Clear error on value touched * tweak(adminPanel): RN-1187: Move secondary labels to tooltips (#5659) * WIP * WIP * Move all seconadry labels to be label tooltips * Autogenerate helper text if has validation rules * Add comment * Fix export * fix endpoint * Fix build * Fix modal headers * Fix spacing * Fix styling of radio labels * Fix build error * Fix data elements permission groups required * Checkbox list validation * Viz builder controlled fields * Fix dashboard relations * Add validation for map overlay group relations * Fix duplicate withConnectedEditor * Fix permission entity input * Change tooltip to label for access requests * Handle validation on multiselect * Clear nested validation errors * Fix dashboard relations validation * Update socialFeed.jsx --------- Co-authored-by: Jasper Lai <33956381+jaskfla@users.noreply.github.com> Co-authored-by: Andrew --- .../DashboardItemMetadataForm.jsx | 100 ++++++--- .../MapOverlay/MapOverlayMetadataForm.jsx | 204 +++++++++++------- .../src/VizBuilderApp/views/CreateNew.jsx | 2 +- .../src/autocomplete/Autocomplete.jsx | 4 + .../src/autocomplete/ReduxAutocomplete.jsx | 6 + packages/admin-panel/src/common.jsx | 6 + .../src/dataTables/DataTableEditFields.jsx | 119 +++++----- packages/admin-panel/src/editor/EditModal.jsx | 155 +++++++------ .../src/editor/EditorInputField.js | 22 ++ .../admin-panel/src/editor/FieldsEditor.jsx | 43 ++-- packages/admin-panel/src/editor/actions.js | 49 +++-- packages/admin-panel/src/editor/constants.js | 1 + packages/admin-panel/src/editor/index.js | 3 + packages/admin-panel/src/editor/reducer.js | 26 ++- .../src/editor/useValidationScroll.js | 38 ++++ packages/admin-panel/src/editor/utils.js | 22 ++ packages/admin-panel/src/editor/validation.js | 119 ++++++++++ .../src/importExport/ImportModal.jsx | 3 +- .../resources/editSurvey/EditSurveyPage.jsx | 42 ++-- .../src/routes/entities/countries.js | 2 + .../src/routes/entities/entities.js | 3 + .../src/routes/externalData/dhisInstances.js | 4 + .../externalDatabaseConnections.js | 2 + .../externalData/msupplySupersetInstances.js | 2 + .../src/routes/projects/landingPages.jsx | 3 + .../src/routes/projects/projects.js | 16 +- .../src/routes/surveys/dataGroups.js | 1 + .../src/routes/surveys/dataMapping.js | 3 + .../src/routes/surveys/optionSets.js | 10 +- .../src/routes/surveys/questions.js | 25 +-- .../src/routes/surveys/surveyResponses.js | 2 +- .../admin-panel/src/routes/surveys/surveys.js | 42 +--- .../src/routes/surveys/syncGroups.js | 4 + .../src/routes/users/accessRequests.js | 5 + .../src/routes/users/permissionGroups.jsx | 2 + .../src/routes/users/permissions.js | 5 + .../admin-panel/src/routes/users/users.jsx | 10 +- .../routes/visualisations/dashboardItems.jsx | 2 + .../visualisations/dashboardMailingLists.js | 13 +- .../visualisations/dashboardRelations.js | 22 +- .../src/routes/visualisations/dashboards.js | 3 + .../src/routes/visualisations/dataTables.js | 20 +- .../src/routes/visualisations/indicators.js | 4 +- .../routes/visualisations/legacyReports.js | 3 + .../mapOverlayGroupRelations.js | 4 + .../routes/visualisations/mapOverlayGroups.js | 2 + .../src/routes/visualisations/mapOverlays.jsx | 4 +- .../src/routes/visualisations/socialFeed.jsx | 2 +- packages/admin-panel/src/theme/theme.js | 6 + .../widgets/InputField/CheckboxListField.jsx | 8 +- .../src/widgets/InputField/InputField.jsx | 54 ++++- .../src/widgets/InputField/InputWrapper.jsx | 66 ++++++ .../src/widgets/InputField/JsonEditor.jsx | 55 +++-- .../src/widgets/InputField/JsonInputField.jsx | 31 ++- .../InputField/registerInputFields.jsx | 71 +++--- .../src/features/Questions/PhotoQuestion.tsx | 2 +- .../src/components/Inputs/HexcodeField.tsx | 14 +- .../components/Inputs/ImageUploadField.tsx | 21 +- .../src/components/Inputs/InputLabel.tsx | 30 ++- .../src/components/Inputs/RadioGroup.tsx | 87 +++++--- .../src/components/Inputs/TextField.tsx | 23 +- 61 files changed, 1129 insertions(+), 523 deletions(-) create mode 100644 packages/admin-panel/src/editor/EditorInputField.js create mode 100644 packages/admin-panel/src/editor/useValidationScroll.js create mode 100644 packages/admin-panel/src/editor/utils.js create mode 100644 packages/admin-panel/src/editor/validation.js create mode 100644 packages/admin-panel/src/widgets/InputField/InputWrapper.jsx diff --git a/packages/admin-panel/src/VizBuilderApp/components/DashboardItem/DashboardItemMetadataForm.jsx b/packages/admin-panel/src/VizBuilderApp/components/DashboardItem/DashboardItemMetadataForm.jsx index bfd93c9238..881f8e4e6a 100644 --- a/packages/admin-panel/src/VizBuilderApp/components/DashboardItem/DashboardItemMetadataForm.jsx +++ b/packages/admin-panel/src/VizBuilderApp/components/DashboardItem/DashboardItemMetadataForm.jsx @@ -4,19 +4,23 @@ */ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { Autocomplete, TextField } from '@tupaia/ui-components'; import { useSearchPermissionGroups } from '../../api/queries'; import { useVizConfigContext } from '../../context'; import { useDebounce } from '../../../utilities'; import { DASHBOARD_ITEM_VIZ_TYPES } from '../../constants'; +import { REQUIRED_FIELD_ERROR } from '../../../editor/validation'; export const DashboardItemMetadataForm = ({ Header, Body, Footer, onSubmit }) => { const vizTypeOptions = Object.entries(DASHBOARD_ITEM_VIZ_TYPES).map(([vizType, { name }]) => ({ value: vizType, label: name, })); - const { handleSubmit, register, errors } = useForm(); + const { handleSubmit, register, errors, control } = useForm({ + mode: 'onChange', + }); + const [ { visualisation, vizType }, { setVisualisationValue, setVizType, setPresentation, setPresentationValue }, @@ -35,11 +39,10 @@ export const DashboardItemMetadataForm = ({ Header, Body, Footer, onSubmit }) => const doSubmit = data => { setVisualisationValue('code', data.code); setVisualisationValue('permissionGroup', data.permissionGroup); - const selectedVizType = vizTypeOptions.find(({ label }) => label === data.vizType).value; - setVizType(selectedVizType); + setVizType(data.vizType.value); if (Object.keys(presentation).length === 0) { // If no presentation config exists, set the initial config by vizType - setPresentation(DASHBOARD_ITEM_VIZ_TYPES[selectedVizType].initialConfig); + setPresentation(DASHBOARD_ITEM_VIZ_TYPES[data.vizType.value].initialConfig); } setPresentationValue('name', data.name); onSubmit(); @@ -54,53 +57,82 @@ export const DashboardItemMetadataForm = ({ Header, Body, Footer, onSubmit }) => label="Code" defaultValue={code} error={!!errors.code} + required helperText={errors.code && errors.code.message} inputRef={register({ - required: 'Required', + required: REQUIRED_FIELD_ERROR, })} /> - p.name)} - disabled={isLoadingPermissionGroups} - error={!!errors.permissionGroup} - helperText={errors.permissionGroup && errors.permissionGroup.message} - inputRef={register({ - required: 'Required', - })} - value={permissionGroupSearchInput} - onInputChange={(event, newValue) => { - setPermissionGroupSearchInput(newValue); + rules={{ required: REQUIRED_FIELD_ERROR }} + render={({ onChange, value, ref, name }) => { + return ( + p.name)} + disabled={isLoadingPermissionGroups} + error={!!errors.permissionGroup} + helperText={errors.permissionGroup && errors.permissionGroup.message} + inputRef={ref} + inputValue={permissionGroupSearchInput} + onInputChange={(event, newValue) => { + setPermissionGroupSearchInput(newValue); + }} + onChange={(event, newValue) => { + onChange(newValue); + }} + value={value} + /> + ); }} /> - value === vizType)} - options={vizTypeOptions} - getOptionLabel={option => option.label} - getOptionSelected={option => option.value} - error={!!errors.vizType} - helperText={errors.vizType && errors.vizType.message} - inputRef={register({ - required: 'Required', - })} + control={control} + required + defaultValue={vizTypeOptions.find(({ value: optionValue }) => optionValue === vizType)} + rules={{ required: REQUIRED_FIELD_ERROR }} + render={({ onChange, value, ref, name }) => ( + option.label} + getOptionSelected={option => { + return option.value === value; + }} + error={!!errors.vizType} + helperText={errors.vizType && errors.vizType.message} + inputRef={ref} + onChange={(event, newValue) => { + onChange(newValue); + }} + value={value} + /> + )} />