diff --git a/anet-dictionary.yml b/anet-dictionary.yml index f651fd3635..945ebae7af 100644 --- a/anet-dictionary.yml +++ b/anet-dictionary.yml @@ -476,13 +476,247 @@ fields: location: format: LAT_LON + customFields: + multipleButtons: + type: enumset + label: Choose one or more of the options + helpText: Help text for choosing multiple values + choices: + opt1: + label: Option 1 + opt2: + label: Option 2 + opt3: + label: Option 3 + inputFieldName: + type: text + label: Text field + placeholder: Placeholder text for input field + helpText: Help text for text field + colourOptions: + type: enum + label: Choose one of the colours + helpText: Help text for choosing colours + choices: + GREEN: + label: Green + color: '#c2ffb3' + AMBER: + label: Amber + color: '#ffe396' + RED: + label: Red + color: '#ff8279' + textareaFieldName: + type: text + label: Textarea field + placeholder: Placeholder text for textarea field + helpText: Help text for textarea field + componentClass: textarea + style: + height: 200px + visibleWhen: $[?(@.colourOptions === 'GREEN')] + numberFieldName: + type: number + typeError: Number field must be a number + label: Number field + placeholder: Placeholder text for number field + helpText: Help text for number field + validations: + - type: integer + - type: min + params: [5] + - type: max + params: [100] + visibleWhen: $[?((@.colourOptions === 'GREEN')||(@.colourOptions === 'RED'))] + nlt: + type: date + label: Not later than date + helpText: Help text for date field + nlt_dt: + type: datetime + label: Not later than datetime + helpText: Help text for datetime field + arrayFieldName: + type: array_of_objects + label: Array of objects + helpText: Here you can add as many objects as needed + addButtonLabel: Add an object + objectLabel: Object + objectFields: + textF: + type: text + label: Object text + placeholder: Placeholder text for object text field + helpText: Help text for object text field + dateF: + type: date + label: Object date + helpText: Help text for object date field + visibleWhen: $[?(@.colourOptions === 'GREEN')] position: name: 'Position Name' + customFields: + multipleButtons: + type: enumset + label: Choose one or more of the options + helpText: Help text for choosing multiple values + choices: + opt1: + label: Option 1 + opt2: + label: Option 2 + opt3: + label: Option 3 + inputFieldName: + type: text + label: Text field + placeholder: Placeholder text for input field + helpText: Help text for text field + colourOptions: + type: enum + label: Choose one of the colours + helpText: Help text for choosing colours + choices: + GREEN: + label: Green + color: '#c2ffb3' + AMBER: + label: Amber + color: '#ffe396' + RED: + label: Red + color: '#ff8279' + textareaFieldName: + type: text + label: Textarea field + placeholder: Placeholder text for textarea field + helpText: Help text for textarea field + componentClass: textarea + style: + height: 200px + visibleWhen: $[?(@.colourOptions === 'GREEN')] + numberFieldName: + type: number + typeError: Number field must be a number + label: Number field + placeholder: Placeholder text for number field + helpText: Help text for number field + validations: + - type: integer + - type: min + params: [5] + - type: max + params: [100] + visibleWhen: $[?((@.colourOptions === 'GREEN')||(@.colourOptions === 'RED'))] + nlt: + type: date + label: Not later than date + helpText: Help text for date field + nlt_dt: + type: datetime + label: Not later than datetime + helpText: Help text for datetime field + arrayFieldName: + type: array_of_objects + label: Array of objects + helpText: Here you can add as many objects as needed + addButtonLabel: Add an object + objectLabel: Object + objectFields: + textF: + type: text + label: Object text + placeholder: Placeholder text for object text field + helpText: Help text for object text field + dateF: + type: date + label: Object date + helpText: Help text for object date field + visibleWhen: $[?(@.colourOptions === 'GREEN')] organization: shortName: Name parentOrg: Parent Organization + customFields: + multipleButtons: + type: enumset + label: Choose one or more of the options + helpText: Help text for choosing multiple values + choices: + opt1: + label: Option 1 + opt2: + label: Option 2 + opt3: + label: Option 3 + inputFieldName: + type: text + label: Text field + placeholder: Placeholder text for input field + helpText: Help text for text field + colourOptions: + type: enum + label: Choose one of the colours + helpText: Help text for choosing colours + choices: + GREEN: + label: Green + color: '#c2ffb3' + AMBER: + label: Amber + color: '#ffe396' + RED: + label: Red + color: '#ff8279' + textareaFieldName: + type: text + label: Textarea field + placeholder: Placeholder text for textarea field + helpText: Help text for textarea field + componentClass: textarea + style: + height: 200px + visibleWhen: $[?(@.colourOptions === 'GREEN')] + numberFieldName: + type: number + typeError: Number field must be a number + label: Number field + placeholder: Placeholder text for number field + helpText: Help text for number field + validations: + - type: integer + - type: min + params: [5] + - type: max + params: [100] + visibleWhen: $[?((@.colourOptions === 'GREEN')||(@.colourOptions === 'RED'))] + nlt: + type: date + label: Not later than date + helpText: Help text for date field + nlt_dt: + type: datetime + label: Not later than datetime + helpText: Help text for datetime field + arrayFieldName: + type: array_of_objects + label: Array of objects + helpText: Here you can add as many objects as needed + addButtonLabel: Add an object + objectLabel: Object + objectFields: + textF: + type: text + label: Object text + placeholder: Placeholder text for object text field + helpText: Help text for object text field + dateF: + type: date + label: Object date + helpText: Help text for object date field + visibleWhen: $[?(@.colourOptions === 'GREEN')] advisor: diff --git a/client/src/components/CustomFields.js b/client/src/components/CustomFields.js index 1d4d4aa94d..3a7434d2d3 100644 --- a/client/src/components/CustomFields.js +++ b/client/src/components/CustomFields.js @@ -660,6 +660,20 @@ const FIELD_COMPONENTS = { [CUSTOM_FIELD_TYPE.ARRAY_OF_ANET_OBJECTS]: ArrayOfAnetObjectsField } +// mutates the object +export function initInvisibleFields( + anetObj, + config, + parentFieldName = DEFAULT_CUSTOM_FIELDS_PARENT +) { + if (anetObj[parentFieldName]) { + // set initial invisible custom fields + anetObj[parentFieldName][ + INVISIBLE_CUSTOM_FIELDS_FIELD + ] = getInvisibleFields(config, parentFieldName, anetObj) + } +} + export function getInvisibleFields( fieldsConfig = {}, parentFieldName, diff --git a/client/src/components/advancedSelectWidget/MultiTypeAdvancedSelectComponent.js b/client/src/components/advancedSelectWidget/MultiTypeAdvancedSelectComponent.js index 709dc84c53..6ce20ebec6 100644 --- a/client/src/components/advancedSelectWidget/MultiTypeAdvancedSelectComponent.js +++ b/client/src/components/advancedSelectWidget/MultiTypeAdvancedSelectComponent.js @@ -74,7 +74,7 @@ const widgetPropsOrganization = { overlayColumns: ["Name"], filterDefs: entityFilters, queryParams: { status: Model.STATUS.ACTIVE }, - fields: Models.Organization.autocompleteQuery, + fields: Models.Organization.autocompleteQueryWithNotes, addon: ORGANIZATIONS_ICON } @@ -84,7 +84,7 @@ const widgetPropsPosition = { overlayColumns: ["Position", "Organization", "Current Occupant"], filterDefs: entityFilters, queryParams: { status: Model.STATUS.ACTIVE }, - fields: Models.Position.autocompleteQuery, + fields: Models.Position.autocompleteQueryWithNotes, addon: POSITIONS_ICON } @@ -94,7 +94,7 @@ const widgetPropsLocation = { overlayColumns: ["Name"], filterDefs: entityFilters, queryParams: { status: Model.STATUS.ACTIVE }, - fields: Models.Location.autocompleteQuery, + fields: Models.Location.autocompleteQueryWithNotes, addon: LOCATIONS_ICON } diff --git a/client/src/models/Location.js b/client/src/models/Location.js index 47361adf1f..98bce1ee4c 100644 --- a/client/src/models/Location.js +++ b/client/src/models/Location.js @@ -1,4 +1,7 @@ -import Model from "components/Model" +import Model, { + createCustomFieldsSchema, + GRAPHQL_NOTES_FIELDS +} from "components/Model" import { convertLatLngToMGRS, convertMGRSToLatLng } from "geoUtils" import _isEmpty from "lodash/isEmpty" import LOCATIONS_ICON from "resources/locations.png" @@ -17,6 +20,11 @@ export default class Location extends Model { REPORT_APPROVAL: "REPORT_APPROVAL" } + // create yup schema for the customFields, based on the customFields config + static customFieldsSchema = createCustomFieldsSchema( + Settings.fields.location.customFields + ) + static yupSchema = yup .object() .shape({ @@ -118,10 +126,14 @@ export default class Location extends Model { .nullable() .default([]) }) + // not actually in the database, the database contains the JSON customFields + .concat(Location.customFieldsSchema) .concat(Model.yupSchema) static autocompleteQuery = "uuid, name" + static autocompleteQueryWithNotes = `${this.autocompleteQuery} ${GRAPHQL_NOTES_FIELDS}` + static hasCoordinates(location) { return ( location && diff --git a/client/src/models/Organization.js b/client/src/models/Organization.js index 8255246e24..2872bc1787 100644 --- a/client/src/models/Organization.js +++ b/client/src/models/Organization.js @@ -1,4 +1,7 @@ -import Model from "components/Model" +import Model, { + createCustomFieldsSchema, + GRAPHQL_NOTES_FIELDS +} from "components/Model" import ORGANIZATIONS_ICON from "resources/organizations.png" import Settings from "settings" import utils from "utils" @@ -20,6 +23,11 @@ export default class Organization extends Model { REPORT_APPROVAL: "REPORT_APPROVAL" } + // create yup schema for the customFields, based on the customFields config + static customFieldsSchema = createCustomFieldsSchema( + Settings.fields.organization.customFields + ) + static yupSchema = yup .object() .shape({ @@ -89,11 +97,15 @@ export default class Organization extends Model { positions: yup.array().nullable().default([]), tasks: yup.array().nullable().default([]) }) + // not actually in the database, the database contains the JSON customFields + .concat(Organization.customFieldsSchema) .concat(Model.yupSchema) static autocompleteQuery = "uuid, shortName, longName, identificationCode, type" + static autocompleteQueryWithNotes = `${this.autocompleteQuery} ${GRAPHQL_NOTES_FIELDS}` + static humanNameOfStatus(status) { return utils.sentenceCase(status) } diff --git a/client/src/models/Position.js b/client/src/models/Position.js index b656a500f7..09ebb23c21 100644 --- a/client/src/models/Position.js +++ b/client/src/models/Position.js @@ -1,4 +1,7 @@ -import Model from "components/Model" +import Model, { + createCustomFieldsSchema, + GRAPHQL_NOTES_FIELDS +} from "components/Model" import AFG_ICON from "resources/afg_small.png" import POSITIONS_ICON from "resources/positions.png" import RS_ICON from "resources/rs_small.png" @@ -24,6 +27,11 @@ export default class Position extends Model { ADMINISTRATOR: "ADMINISTRATOR" } + // create yup schema for the customFields, based on the customFields config + static customFieldsSchema = createCustomFieldsSchema( + Settings.fields.position.customFields + ) + static yupSchema = yup .object() .shape({ @@ -57,11 +65,15 @@ export default class Position extends Model { person: yup.object().nullable().default({}), location: yup.object().nullable().default({}) }) + // not actually in the database, the database contains the JSON customFields + .concat(Position.customFieldsSchema) .concat(Model.yupSchema) static autocompleteQuery = "uuid, name, code, type, status, organization { uuid, shortName}, person { uuid, name, rank, role, avatar(size: 32) }" + static autocompleteQueryWithNotes = `${this.autocompleteQuery} ${GRAPHQL_NOTES_FIELDS}` + static humanNameOfStatus(status) { return utils.sentenceCase(status) } diff --git a/client/src/pages/admin/authorizationgroup/Form.js b/client/src/pages/admin/authorizationgroup/Form.js index 6d32357adc..8d6df87ea6 100644 --- a/client/src/pages/admin/authorizationgroup/Form.js +++ b/client/src/pages/admin/authorizationgroup/Form.js @@ -5,7 +5,7 @@ import { PositionOverlayRow } from "components/advancedSelectWidget/AdvancedSele import * as FieldHelper from "components/FieldHelper" import Fieldset from "components/Fieldset" import Messages from "components/Messages" -import Model from "components/Model" +import Model, { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import NavigationWarning from "components/NavigationWarning" import { jumpToTop } from "components/Page" import PositionTable from "components/PositionTable" @@ -233,6 +233,14 @@ const AuthorizationGroupForm = ({ edit, title, initialValues }) => { new AuthorizationGroup(values), "notes" ) + authorizationGroup.positions = values.positions.map(pos => { + const p = Object.without( + pos, + "customFields", + DEFAULT_CUSTOM_FIELDS_PARENT + ) + return p + }) return API.mutation( edit ? GQL_UPDATE_AUTHORIZATION_GROUP : GQL_CREATE_AUTHORIZATION_GROUP, { authorizationGroup } diff --git a/client/src/pages/locations/Edit.js b/client/src/pages/locations/Edit.js index d65e4052bb..701c70a384 100644 --- a/client/src/pages/locations/Edit.js +++ b/client/src/pages/locations/Edit.js @@ -1,9 +1,11 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import RelatedObjectNotes, { @@ -13,6 +15,8 @@ import { Location } from "models" import React from "react" import { connect } from "react-redux" import { useParams } from "react-router-dom" +import Settings from "settings" +import utils from "utils" import LocationForm from "./Form" const GQL_GET_LOCATION = gql` @@ -52,7 +56,8 @@ const GQL_GET_LOCATION = gql` avatar(size: 32) } } - } + } + customFields ${GRAPHQL_NOTES_FIELDS} } } @@ -75,8 +80,14 @@ const LocationEdit = ({ pageDispatchers }) => { if (done) { return result } - + if (data) { + data.location[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.location.customFields + ) + } const location = new Location(data ? data.location : {}) + // mutates the object + initInvisibleFields(location, Settings.fields.location.customFields) return (
diff --git a/client/src/pages/locations/Form.js b/client/src/pages/locations/Form.js index 8eeada0f6c..568b2b1933 100644 --- a/client/src/pages/locations/Form.js +++ b/client/src/pages/locations/Form.js @@ -2,11 +2,15 @@ import API from "api" import { gql } from "apollo-boost" import AppContext from "components/AppContext" import ApprovalsDefinition from "components/approvals/ApprovalsDefinition" +import { + CustomFieldsContainer, + customFieldsJSONString +} from "components/CustomFields" import * as FieldHelper from "components/FieldHelper" import Fieldset from "components/Fieldset" import Leaflet from "components/Leaflet" import Messages from "components/Messages" -import Model from "components/Model" +import Model, { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import NavigationWarning from "components/NavigationWarning" import { jumpToTop } from "components/Page" import { FastField, Form, Formik } from "formik" @@ -17,6 +21,7 @@ import PropTypes from "prop-types" import React, { useContext, useState } from "react" import { Button } from "react-bootstrap" import { useHistory } from "react-router-dom" +import Settings from "settings" import GeoLocation from "./GeoLocation" const GQL_CREATE_LOCATION = gql` @@ -94,6 +99,7 @@ const LocationForm = ({ edit, title, initialValues }) => { setFieldValue, setValues, values, + validateForm, submitForm }) => { const marker = { @@ -189,7 +195,19 @@ const LocationForm = ({ edit, title, initialValues }) => { setFieldValue={setFieldValue} approversFilters={approversFilters} /> - + {Settings.fields.location.customFields && ( +
+ +
+ )}
@@ -260,8 +278,11 @@ const LocationForm = ({ edit, title, initialValues }) => { const location = Object.without( new Location(values), "notes", - "displayedCoordinate" + "displayedCoordinate", + "customFields", // initial JSON from the db + DEFAULT_CUSTOM_FIELDS_PARENT ) + location.customFields = customFieldsJSONString(values) return API.mutation(edit ? GQL_UPDATE_LOCATION : GQL_CREATE_LOCATION, { location }) diff --git a/client/src/pages/locations/New.js b/client/src/pages/locations/New.js index 45adf8e392..784e860256 100644 --- a/client/src/pages/locations/New.js +++ b/client/src/pages/locations/New.js @@ -1,12 +1,14 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" +import { initInvisibleFields } from "components/CustomFields" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import { Location } from "models" import React from "react" import { connect } from "react-redux" +import Settings from "settings" import LocationForm from "./Form" const LocationNew = ({ pageDispatchers }) => { @@ -17,7 +19,8 @@ const LocationNew = ({ pageDispatchers }) => { }) const location = new Location() - + // mutates the object + initInvisibleFields(location, Settings.fields.location.customFields) return } diff --git a/client/src/pages/locations/Show.js b/client/src/pages/locations/Show.js index ed79a1edaa..5e03fbedd7 100644 --- a/client/src/pages/locations/Show.js +++ b/client/src/pages/locations/Show.js @@ -3,11 +3,13 @@ import API from "api" import { gql } from "apollo-boost" import AppContext from "components/AppContext" import Approvals from "components/approvals/Approvals" +import { ReadonlyCustomFields } from "components/CustomFields" import * as FieldHelper from "components/FieldHelper" import Fieldset from "components/Fieldset" import Leaflet from "components/Leaflet" import LinkTo from "components/LinkTo" import Messages from "components/Messages" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -24,6 +26,8 @@ import { Location } from "models" import React, { useContext } from "react" import { connect } from "react-redux" import { useLocation, useParams } from "react-router-dom" +import Settings from "settings" +import utils from "utils" import GeoLocation, { GEO_LOCATION_DISPLAY_TYPE } from "./GeoLocation" const GQL_GET_LOCATION = gql` @@ -64,6 +68,7 @@ const GQL_GET_LOCATION = gql` } } } + customFields ${GRAPHQL_NOTES_FIELDS} } } @@ -88,7 +93,11 @@ const LocationShow = ({ pageDispatchers }) => { if (done) { return result } - + if (data) { + data.location[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.location.customFields + ) + } const location = new Location(data ? data.location : {}) const stateSuccess = routerLocation.state && routerLocation.state.success const stateError = routerLocation.state && routerLocation.state.error @@ -156,6 +165,14 @@ const LocationShow = ({ pageDispatchers }) => { + {Settings.fields.location.customFields && ( +
+ +
+ )} diff --git a/client/src/pages/organizations/Edit.js b/client/src/pages/organizations/Edit.js index c29b0a647c..c7fb4b6929 100644 --- a/client/src/pages/organizations/Edit.js +++ b/client/src/pages/organizations/Edit.js @@ -1,9 +1,11 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import RelatedObjectNotes, { @@ -13,6 +15,8 @@ import { Organization } from "models" import React from "react" import { connect } from "react-redux" import { useParams } from "react-router-dom" +import Settings from "settings" +import utils from "utils" import OrganizationForm from "./Form" const GQL_GET_ORGANIZATION = gql` @@ -65,6 +69,7 @@ const GQL_GET_ORGANIZATION = gql` shortName longName } + customFields ${GRAPHQL_NOTES_FIELDS} } } @@ -87,9 +92,14 @@ const OrganizationEdit = ({ pageDispatchers }) => { if (done) { return result } - + if (data) { + data.organization[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.organization.customFields + ) + } const organization = new Organization(data ? data.organization : {}) - + // mutates the object + initInvisibleFields(organization, Settings.fields.organization.customFields) return (
{ setFieldValue, setFieldTouched, values, + validateForm, submitForm }) => { const isAdmin = currentUser && currentUser.isAdmin() @@ -349,6 +354,19 @@ const OrganizationForm = ({ edit, title, initialValues }) => { )}
)} + {Settings.fields.organization.customFields && ( +
+ +
+ )}
@@ -413,11 +431,14 @@ const OrganizationForm = ({ edit, title, initialValues }) => { "notes", "childrenOrgs", "positions", - "tasks" + "tasks", + "customFields", // initial JSON from the db + DEFAULT_CUSTOM_FIELDS_PARENT ) // strip tasks fields not in data model organization.tasks = values.tasks.map(t => utils.getReference(t)) organization.parentOrg = utils.getReference(organization.parentOrg) + organization.customFields = customFieldsJSONString(values) return API.mutation( edit ? GQL_UPDATE_ORGANIZATION : GQL_CREATE_ORGANIZATION, { organization } diff --git a/client/src/pages/organizations/New.js b/client/src/pages/organizations/New.js index dc1acac6f8..4fea87b745 100644 --- a/client/src/pages/organizations/New.js +++ b/client/src/pages/organizations/New.js @@ -1,9 +1,10 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" +import { initInvisibleFields } from "components/CustomFields" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import { Organization } from "models" @@ -11,6 +12,7 @@ import PropTypes from "prop-types" import React from "react" import { connect } from "react-redux" import { useLocation } from "react-router-dom" +import Settings from "settings" import utils from "utils" import OrganizationForm from "./Form" @@ -87,7 +89,8 @@ const OrganizationNewConditional = ({ organization.parentOrg = new Organization(data.organization) organization.type = organization.parentOrg.type } - + // mutates the object + initInvisibleFields(organization, Settings.fields.organization.customFields) return ( { if (done) { return result } - + if (data) { + data.organization[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.organization.customFields + ) + } const organization = new Organization(data ? data.organization : {}) const stateSuccess = routerLocation.state && routerLocation.state.success const stateError = routerLocation.state && routerLocation.state.error @@ -419,6 +426,14 @@ const OrganizationShow = ({ pageDispatchers }) => { } /> + {Settings.fields.organization.customFields && ( +
+ +
+ )}
) diff --git a/client/src/pages/people/Edit.js b/client/src/pages/people/Edit.js index c04c13ad30..7a3b4ed003 100644 --- a/client/src/pages/people/Edit.js +++ b/client/src/pages/people/Edit.js @@ -1,11 +1,8 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" -import { getInvisibleFields } from "components/CustomFields" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -94,16 +91,8 @@ const PersonEdit = ({ pageDispatchers }) => { ? "Update profile" : "Save Person" - if (person[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - person[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.person.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - person - ) - } + // mutates the object + initInvisibleFields(person, Settings.fields.person.customFields) return (
diff --git a/client/src/pages/people/New.js b/client/src/pages/people/New.js index efc1d9ea77..a4e9505c96 100644 --- a/client/src/pages/people/New.js +++ b/client/src/pages/people/New.js @@ -1,9 +1,5 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" -import { getInvisibleFields } from "components/CustomFields" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" +import { initInvisibleFields } from "components/CustomFields" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -24,16 +20,8 @@ const PersonNew = ({ pageDispatchers }) => { const person = new Person() - if (person[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - person[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.person.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - person - ) - } + // mutates the object + initInvisibleFields(person, Settings.fields.person.customFields) return } diff --git a/client/src/pages/positions/Edit.js b/client/src/pages/positions/Edit.js index b9a1617fc2..9a3b0d7789 100644 --- a/client/src/pages/positions/Edit.js +++ b/client/src/pages/positions/Edit.js @@ -1,9 +1,11 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import RelatedObjectNotes, { @@ -13,6 +15,8 @@ import { Position } from "models" import React from "react" import { connect } from "react-redux" import { useParams } from "react-router-dom" +import Settings from "settings" +import utils from "utils" import PositionForm from "./Form" const GQL_GET_POSITION = gql` @@ -53,6 +57,7 @@ const GQL_GET_POSITION = gql` role avatar(size: 32) } + customFields ${GRAPHQL_NOTES_FIELDS} } } @@ -76,7 +81,15 @@ const PositionEdit = ({ pageDispatchers }) => { return result } + if (data) { + data.position[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.position.customFields + ) + } + const position = new Position(data ? data.position : {}) + // mutates the object + initInvisibleFields(position, Settings.fields.position.customFields) return (
diff --git a/client/src/pages/positions/Form.js b/client/src/pages/positions/Form.js index cd3ead791b..9e97edd8b0 100644 --- a/client/src/pages/positions/Form.js +++ b/client/src/pages/positions/Form.js @@ -6,11 +6,15 @@ import { } from "components/advancedSelectWidget/AdvancedSelectOverlayRow" import AdvancedSingleSelect from "components/advancedSelectWidget/AdvancedSingleSelect" import AppContext from "components/AppContext" +import { + CustomFieldsContainer, + customFieldsJSONString +} from "components/CustomFields" import * as FieldHelper from "components/FieldHelper" import Fieldset from "components/Fieldset" import LinkTo from "components/LinkTo" import Messages from "components/Messages" -import Model from "components/Model" +import Model, { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import NavigationWarning from "components/NavigationWarning" import { jumpToTop } from "components/Page" import { FastField, Field, Form, Formik } from "formik" @@ -110,13 +114,12 @@ const PositionForm = ({ edit, title, initialValues }) => { initialValues={initialValues} > {({ - handleSubmit, isSubmitting, dirty, - errors, setFieldValue, setFieldTouched, values, + validateForm, submitForm }) => { const isPrincipal = values.type === Position.TYPE.PRINCIPAL @@ -298,7 +301,19 @@ const PositionForm = ({ edit, title, initialValues }) => { } /> - + {Settings.fields.position.customFields && ( +
+ +
+ )}
@@ -358,7 +373,9 @@ const PositionForm = ({ edit, title, initialValues }) => { const position = Object.without( new Position(values), "notes", - "responsibleTasks" // Only for querying + "customFields", // initial JSON from the db + "responsibleTasks", // Only for querying + DEFAULT_CUSTOM_FIELDS_PARENT ) if (position.type !== Position.TYPE.PRINCIPAL) { position.type = position.permissions || Position.TYPE.ADVISOR @@ -370,6 +387,8 @@ const PositionForm = ({ edit, title, initialValues }) => { position.organization = utils.getReference(position.organization) position.person = utils.getReference(position.person) position.code = position.code || null // Need to null out empty position codes + position.customFields = customFieldsJSONString(values) + return API.mutation(edit ? GQL_UPDATE_POSITION : GQL_CREATE_POSITION, { position }) diff --git a/client/src/pages/positions/New.js b/client/src/pages/positions/New.js index cdecae7bf3..743f0594f3 100644 --- a/client/src/pages/positions/New.js +++ b/client/src/pages/positions/New.js @@ -1,9 +1,10 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" +import { initInvisibleFields } from "components/CustomFields" import { - PageDispatchersPropType, mapPageDispatchersToProps, + PageDispatchersPropType, useBoilerplate } from "components/Page" import { Organization, Position } from "models" @@ -11,6 +12,7 @@ import PropTypes from "prop-types" import React from "react" import { connect } from "react-redux" import { useLocation } from "react-router-dom" +import Settings from "settings" import utils from "utils" import PositionForm from "./Form" @@ -93,6 +95,9 @@ const PositionNewConditional = ({ : Position.TYPE.PRINCIPAL } + // mutates the object + initInvisibleFields(position, Settings.fields.position.customFields) + return } diff --git a/client/src/pages/positions/Show.js b/client/src/pages/positions/Show.js index 6743288306..d5a4f887c6 100644 --- a/client/src/pages/positions/Show.js +++ b/client/src/pages/positions/Show.js @@ -4,13 +4,14 @@ import { gql } from "apollo-boost" import AppContext from "components/AppContext" import AssignPersonModal from "components/AssignPersonModal" import ConfirmDelete from "components/ConfirmDelete" +import { ReadonlyCustomFields } from "components/CustomFields" import EditAssociatedPositionsModal from "components/EditAssociatedPositionsModal" import * as FieldHelper from "components/FieldHelper" import Fieldset from "components/Fieldset" import GuidedTour from "components/GuidedTour" import LinkTo from "components/LinkTo" import Messages from "components/Messages" -import Model from "components/Model" +import Model, { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { jumpToTop, mapPageDispatchersToProps, @@ -30,6 +31,7 @@ import { Button, Table } from "react-bootstrap" import { connect } from "react-redux" import { useHistory, useLocation, useParams } from "react-router-dom" import Settings from "settings" +import utils from "utils" const GQL_GET_POSITION = gql` query($uuid: String!) { @@ -83,6 +85,7 @@ const GQL_GET_POSITION = gql` uuid name } + customFields ${GRAPHQL_NOTES_FIELDS} } @@ -124,6 +127,12 @@ const PositionShow = ({ pageDispatchers }) => { return result } + if (data) { + data.position[DEFAULT_CUSTOM_FIELDS_PARENT] = utils.parseJsonSafe( + data.position.customFields + ) + } + const position = new Position(data ? data.position : {}) const CodeFieldWithLabel = DictionaryField(Field) @@ -369,6 +378,14 @@ const PositionShow = ({ pageDispatchers }) => { + {Settings.fields.position.customFields && ( +
+ +
+ )} {canDelete && ( diff --git a/client/src/pages/reports/Edit.js b/client/src/pages/reports/Edit.js index 12eb432aab..d5937ecf62 100644 --- a/client/src/pages/reports/Edit.js +++ b/client/src/pages/reports/Edit.js @@ -1,11 +1,8 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" -import { getInvisibleFields } from "components/CustomFields" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -139,16 +136,8 @@ const ReportEdit = ({ pageDispatchers }) => { report.getAttendeesEngagementAssessments() ) - if (reportInitialValues[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - reportInitialValues[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.report.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - report - ) - } + // mutates the object + initInvisibleFields(reportInitialValues, Settings.fields.report.customFields) reportInitialValues.tasks = Task.fromArray(reportInitialValues.tasks) reportInitialValues.reportPeople = Person.fromArray( diff --git a/client/src/pages/reports/New.js b/client/src/pages/reports/New.js index e3468c2515..8d88521276 100644 --- a/client/src/pages/reports/New.js +++ b/client/src/pages/reports/New.js @@ -1,11 +1,7 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import AppContext from "components/AppContext" -import { getInvisibleFields } from "components/CustomFields" +import { initInvisibleFields } from "components/CustomFields" import GuidedTour from "components/GuidedTour" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -28,16 +24,8 @@ const ReportNew = ({ pageDispatchers }) => { const report = new Report() - if (report[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - report[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.report.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - report - ) - } + // mutates the object + initInvisibleFields(report, Settings.fields.report.customFields) if (currentUser && currentUser.uuid) { const person = new Person(currentUser) diff --git a/client/src/pages/tasks/Edit.js b/client/src/pages/tasks/Edit.js index 11a86463b3..e4f542ab85 100644 --- a/client/src/pages/tasks/Edit.js +++ b/client/src/pages/tasks/Edit.js @@ -1,11 +1,8 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" -import { getInvisibleFields } from "components/CustomFields" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" +import { initInvisibleFields } from "components/CustomFields" +import { DEFAULT_CUSTOM_FIELDS_PARENT } from "components/Model" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -125,16 +122,8 @@ const TaskEdit = ({ pageDispatchers }) => { } const task = new Task(data ? data.task : {}) - if (task[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - task[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.task.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - task - ) - } + // mutates the object + initInvisibleFields(task, Settings.fields.task.customFields) return (
diff --git a/client/src/pages/tasks/New.js b/client/src/pages/tasks/New.js index e8f4145297..363fafff94 100644 --- a/client/src/pages/tasks/New.js +++ b/client/src/pages/tasks/New.js @@ -1,11 +1,7 @@ import { DEFAULT_SEARCH_PROPS, PAGE_PROPS_NO_NAV } from "actions" import API from "api" import { gql } from "apollo-boost" -import { getInvisibleFields } from "components/CustomFields" -import { - DEFAULT_CUSTOM_FIELDS_PARENT, - INVISIBLE_CUSTOM_FIELDS_FIELD -} from "components/Model" +import { initInvisibleFields } from "components/CustomFields" import { mapPageDispatchersToProps, PageDispatchersPropType, @@ -92,17 +88,8 @@ const TaskNewConditional = ({ if (data) { task.taskedOrganizations = [new Organization(data.organization)] } - - if (task[DEFAULT_CUSTOM_FIELDS_PARENT]) { - // set initial invisible custom fields - task[DEFAULT_CUSTOM_FIELDS_PARENT][ - INVISIBLE_CUSTOM_FIELDS_FIELD - ] = getInvisibleFields( - Settings.fields.task.customFields, - DEFAULT_CUSTOM_FIELDS_PARENT, - task - ) - } + // mutates the object + initInvisibleFields(task, Settings.fields.task.customFields) return ( getByIds(List uuids) { @Override public Location insertInternal(Location l) { getDbHandle().createUpdate( - "/* locationInsert */ INSERT INTO locations (uuid, name, status, lat, lng, \"createdAt\", \"updatedAt\") " - + "VALUES (:uuid, :name, :status, :lat, :lng, :createdAt, :updatedAt)") + "/* locationInsert */ INSERT INTO locations (uuid, name, status, lat, lng, \"createdAt\", " + + "\"updatedAt\", \"customFields\") VALUES (:uuid, :name, :status, :lat, :lng, :createdAt, " + + ":updatedAt, :customFields)") .bindBean(l).bind("createdAt", DaoUtils.asLocalDateTime(l.getCreatedAt())) .bind("updatedAt", DaoUtils.asLocalDateTime(l.getUpdatedAt())) .bind("status", DaoUtils.getEnumId(l.getStatus())).execute(); @@ -48,8 +49,9 @@ public Location insertInternal(Location l) { @Override public int updateInternal(Location l) { return getDbHandle().createUpdate("/* updateLocation */ UPDATE locations " - + "SET name = :name, status = :status, lat = :lat, lng = :lng, \"updatedAt\" = :updatedAt WHERE uuid = :uuid") - .bindBean(l).bind("updatedAt", DaoUtils.asLocalDateTime(l.getUpdatedAt())) + + "SET name = :name, status = :status, lat = :lat, lng = :lng, \"updatedAt\" = :updatedAt, " + + "\"customFields\" = :customFields WHERE uuid = :uuid").bindBean(l) + .bind("updatedAt", DaoUtils.asLocalDateTime(l.getUpdatedAt())) .bind("status", DaoUtils.getEnumId(l.getStatus())).execute(); } diff --git a/src/main/java/mil/dds/anet/database/OrganizationDao.java b/src/main/java/mil/dds/anet/database/OrganizationDao.java index 11af06a81e..8f10619058 100644 --- a/src/main/java/mil/dds/anet/database/OrganizationDao.java +++ b/src/main/java/mil/dds/anet/database/OrganizationDao.java @@ -25,7 +25,7 @@ public class OrganizationDao extends AnetBaseDao { private static String[] fields = {"uuid", "shortName", "longName", "status", "identificationCode", - "type", "createdAt", "updatedAt", "parentOrgUuid"}; + "type", "createdAt", "updatedAt", "parentOrgUuid", "customFields"}; public static String TABLE_NAME = "organizations"; public static String ORGANIZATION_FIELDS = DaoUtils.buildFieldAliases(TABLE_NAME, fields, true); @@ -116,9 +116,13 @@ public List getOrgsByShortNames(List shortNames) { @Override public Organization insertInternal(Organization org) { - getDbHandle().createUpdate( - "/* insertOrg */ INSERT INTO organizations (uuid, \"shortName\", \"longName\", status, \"identificationCode\", type, \"createdAt\", \"updatedAt\", \"parentOrgUuid\") " - + "VALUES (:uuid, :shortName, :longName, :status, :identificationCode, :type, :createdAt, :updatedAt, :parentOrgUuid)") + getDbHandle() + .createUpdate( + "/* insertOrg */ INSERT INTO organizations (uuid, \"shortName\", \"longName\", status, " + + "\"identificationCode\", type, \"createdAt\", \"updatedAt\", \"parentOrgUuid\", " + + "\"customFields\") VALUES (:uuid, :shortName, :longName, :status, " + + ":identificationCode, :type, :createdAt, :updatedAt, :parentOrgUuid, " + + ":customFields)") .bindBean(org).bind("createdAt", DaoUtils.asLocalDateTime(org.getCreatedAt())) .bind("updatedAt", DaoUtils.asLocalDateTime(org.getUpdatedAt())) .bind("status", DaoUtils.getEnumId(org.getStatus())) @@ -129,9 +133,12 @@ public Organization insertInternal(Organization org) { @Override public int updateInternal(Organization org) { - return getDbHandle().createUpdate("/* updateOrg */ UPDATE organizations " - + "SET \"shortName\" = :shortName, \"longName\" = :longName, status = :status, \"identificationCode\" = :identificationCode, type = :type, " - + "\"updatedAt\" = :updatedAt, \"parentOrgUuid\" = :parentOrgUuid where uuid = :uuid") + return getDbHandle() + .createUpdate("/* updateOrg */ UPDATE organizations " + + "SET \"shortName\" = :shortName, \"longName\" = :longName, status = :status, " + + "\"identificationCode\" = :identificationCode, type = :type, " + + "\"updatedAt\" = :updatedAt, \"parentOrgUuid\" = :parentOrgUuid, " + + "\"customFields\" = :customFields WHERE uuid = :uuid") .bindBean(org).bind("updatedAt", DaoUtils.asLocalDateTime(org.getUpdatedAt())) .bind("status", DaoUtils.getEnumId(org.getStatus())) .bind("type", DaoUtils.getEnumId(org.getType())) diff --git a/src/main/java/mil/dds/anet/database/PersonDao.java b/src/main/java/mil/dds/anet/database/PersonDao.java index 792e54acf4..8f75f28f37 100644 --- a/src/main/java/mil/dds/anet/database/PersonDao.java +++ b/src/main/java/mil/dds/anet/database/PersonDao.java @@ -139,10 +139,11 @@ public List> getPersonPositionHistory(List f public Person insertInternal(Person p) { StringBuilder sql = new StringBuilder(); sql.append("/* personInsert */ INSERT INTO people " - + "(uuid, name, status, role, \"emailAddress\", \"phoneNumber\", rank, \"pendingVerification\", " - + "gender, country, avatar, code, \"endOfTourDate\", biography, \"domainUsername\", \"createdAt\", \"updatedAt\", \"customFields\") " - + "VALUES (:uuid, :name, :status, :role, :emailAddress, :phoneNumber, :rank, :pendingVerification, " - + ":gender, :country, :avatar, :code, "); + + "(uuid, name, status, role, \"emailAddress\", \"phoneNumber\", rank, " + + "\"pendingVerification\", gender, country, avatar, code, \"endOfTourDate\", biography, " + + "\"domainUsername\", \"createdAt\", \"updatedAt\", \"customFields\") " + + "VALUES (:uuid, :name, :status, :role, :emailAddress, :phoneNumber, :rank, " + + ":pendingVerification, :gender, :country, :avatar, :code, "); if (DaoUtils.isMsSql()) { // MsSql requires an explicit CAST when datetime2 might be NULL. sql.append("CAST(:endOfTourDate AS datetime2), "); @@ -163,9 +164,8 @@ public Person insertInternal(Person p) { @Override public int updateInternal(Person p) { StringBuilder sql = new StringBuilder("/* personUpdate */ UPDATE people " - + "SET name = :name, status = :status, role = :role, " - + "gender = :gender, country = :country, \"emailAddress\" = :emailAddress, " - + "\"avatar\" = :avatar, code = :code, " + + "SET name = :name, status = :status, role = :role, gender = :gender, country = :country, " + + "\"emailAddress\" = :emailAddress, \"avatar\" = :avatar, code = :code, " + "\"phoneNumber\" = :phoneNumber, rank = :rank, biography = :biography, " + "\"pendingVerification\" = :pendingVerification, \"domainUsername\" = :domainUsername, " + "\"updatedAt\" = :updatedAt, \"customFields\" = :customFields, "); diff --git a/src/main/java/mil/dds/anet/database/PositionDao.java b/src/main/java/mil/dds/anet/database/PositionDao.java index 8b46314da6..dffa8630c2 100644 --- a/src/main/java/mil/dds/anet/database/PositionDao.java +++ b/src/main/java/mil/dds/anet/database/PositionDao.java @@ -31,7 +31,7 @@ public class PositionDao extends AnetBaseDao { public static String[] fields = {"uuid", "name", "code", "createdAt", "updatedAt", - "organizationUuid", "currentPersonUuid", "type", "status", "locationUuid"}; + "organizationUuid", "currentPersonUuid", "type", "status", "locationUuid", "customFields"}; public static String TABLE_NAME = "positions"; public static String POSITIONS_FIELDS = DaoUtils.buildFieldAliases(TABLE_NAME, fields, true); @@ -45,8 +45,9 @@ public Position insertInternal(Position p) { try { getDbHandle() .createUpdate("/* positionInsert */ INSERT INTO positions (uuid, name, code, type, " - + "status, \"organizationUuid\", \"locationUuid\", \"createdAt\", \"updatedAt\") " - + "VALUES (:uuid, :name, :code, :type, :status, :organizationUuid, :locationUuid, :createdAt, :updatedAt)") + + "status, \"organizationUuid\", \"locationUuid\", \"createdAt\", \"updatedAt\", " + + "\"customFields\") VALUES (:uuid, :name, :code, :type, :status, :organizationUuid, " + + ":locationUuid, :createdAt, :updatedAt, :customFields)") .bindBean(p).bind("createdAt", DaoUtils.asLocalDateTime(p.getCreatedAt())) .bind("updatedAt", DaoUtils.asLocalDateTime(p.getUpdatedAt())) .bind("type", DaoUtils.getEnumId(p.getType())) @@ -157,9 +158,10 @@ public int updateInternal(Position p) { try { final int nr = getDbHandle() - .createUpdate("/* positionUpdate */ UPDATE positions SET name = :name, " - + "code = :code, \"organizationUuid\" = :organizationUuid, type = :type, status = :status, " - + "\"locationUuid\" = :locationUuid, \"updatedAt\" = :updatedAt WHERE uuid = :uuid") + .createUpdate("/* positionUpdate */ UPDATE positions SET name = :name, code = :code, " + + "\"organizationUuid\" = :organizationUuid, type = :type, status = :status, " + + "\"locationUuid\" = :locationUuid, \"updatedAt\" = :updatedAt, " + + "\"customFields\" = :customFields WHERE uuid = :uuid") .bindBean(p).bind("updatedAt", DaoUtils.asLocalDateTime(p.getUpdatedAt())) .bind("type", DaoUtils.getEnumId(p.getType())) .bind("status", DaoUtils.getEnumId(p.getStatus())).execute(); diff --git a/src/main/java/mil/dds/anet/database/ReportDao.java b/src/main/java/mil/dds/anet/database/ReportDao.java index 41a0ea8f29..063979aefb 100644 --- a/src/main/java/mil/dds/anet/database/ReportDao.java +++ b/src/main/java/mil/dds/anet/database/ReportDao.java @@ -123,8 +123,8 @@ public Report insertInternal(Report r, Person user) { } else { sql.append(":engagementDate, :releasedAt, "); } - sql.append( - ":duration, :atmosphere, :cancelledReason, :atmosphereDetails, :advisorOrgUuid, :principalOrgUuid, :customFields)"); + sql.append(":duration, :atmosphere, :cancelledReason, :atmosphereDetails, :advisorOrgUuid, " + + ":principalOrgUuid, :customFields)"); getDbHandle().createUpdate(sql.toString()).bindBean(r) .bind("createdAt", DaoUtils.asLocalDateTime(r.getCreatedAt())) @@ -226,20 +226,20 @@ public int updateInternal(Report r, Person user) { StringBuilder sql = new StringBuilder("/* updateReport */ UPDATE reports SET " + "state = :state, \"updatedAt\" = :updatedAt, \"locationUuid\" = :locationUuid, " - + "intent = :intent, exsum = :exsum, text = :reportText, " - + "\"keyOutcomes\" = :keyOutcomes, \"nextSteps\" = :nextSteps, " - + "\"approvalStepUuid\" = :approvalStepUuid, "); + + "intent = :intent, exsum = :exsum, text = :reportText, \"keyOutcomes\" = :keyOutcomes, " + + "\"nextSteps\" = :nextSteps, \"approvalStepUuid\" = :approvalStepUuid, "); if (DaoUtils.isMsSql()) { - sql.append( - "\"engagementDate\" = CAST(:engagementDate AS datetime2), \"releasedAt\" = CAST(:releasedAt AS datetime2), "); + sql.append("\"engagementDate\" = CAST(:engagementDate AS datetime2), " + + "\"releasedAt\" = CAST(:releasedAt AS datetime2), "); } else { sql.append("\"engagementDate\" = :engagementDate, \"releasedAt\" = :releasedAt, "); } - sql.append( - "duration = :duration, atmosphere = :atmosphere, \"atmosphereDetails\" = :atmosphereDetails, " - + "\"cancelledReason\" = :cancelledReason, " - + "\"principalOrganizationUuid\" = :principalOrgUuid, \"advisorOrganizationUuid\" = :advisorOrgUuid, " - + "\"customFields\" = :customFields " + "WHERE uuid = :uuid"); + sql.append("duration = :duration, atmosphere = :atmosphere, " + + "\"atmosphereDetails\" = :atmosphereDetails, " + + "\"cancelledReason\" = :cancelledReason, " + + "\"principalOrganizationUuid\" = :principalOrgUuid, " + + "\"advisorOrganizationUuid\" = :advisorOrgUuid, " + + "\"customFields\" = :customFields WHERE uuid = :uuid"); return getDbHandle().createUpdate(sql.toString()).bindBean(r) .bind("updatedAt", DaoUtils.asLocalDateTime(r.getUpdatedAt())) diff --git a/src/main/java/mil/dds/anet/database/TaskDao.java b/src/main/java/mil/dds/anet/database/TaskDao.java index 3a1cf77d1d..91ad744bc1 100644 --- a/src/main/java/mil/dds/anet/database/TaskDao.java +++ b/src/main/java/mil/dds/anet/database/TaskDao.java @@ -108,11 +108,13 @@ public CompletableFuture> getTasksBySearch(Map contex @Override public Task insertInternal(Task p) { getDbHandle().createUpdate("/* insertTask */ INSERT INTO tasks " - + "(uuid, \"longName\", \"shortName\", category, \"customFieldRef1Uuid\", \"createdAt\", \"updatedAt\", status, " - + "\"customField\", \"customFieldEnum1\", \"customFieldEnum2\", \"plannedCompletion\", \"projectedCompletion\", \"customFields\") " - + "VALUES (:uuid, :longName, :shortName, :category, :customFieldRef1Uuid, :createdAt, :updatedAt, :status, " - + ":customField, :customFieldEnum1, :customFieldEnum2, :plannedCompletion, :projectedCompletion, :customFields)") - .bindBean(p).bind("createdAt", DaoUtils.asLocalDateTime(p.getCreatedAt())) + + "(uuid, \"longName\", \"shortName\", category, \"customFieldRef1Uuid\", \"createdAt\", " + + "\"updatedAt\", status, \"customField\", \"customFieldEnum1\", \"customFieldEnum2\", " + + "\"plannedCompletion\", \"projectedCompletion\", \"customFields\") " + + "VALUES (:uuid, :longName, :shortName, :category, :customFieldRef1Uuid, :createdAt, " + + ":updatedAt, :status, :customField, :customFieldEnum1, :customFieldEnum2, " + + ":plannedCompletion, :projectedCompletion, :customFields)").bindBean(p) + .bind("createdAt", DaoUtils.asLocalDateTime(p.getCreatedAt())) .bind("updatedAt", DaoUtils.asLocalDateTime(p.getUpdatedAt())) .bind("plannedCompletion", DaoUtils.asLocalDateTime(p.getPlannedCompletion())) .bind("projectedCompletion", DaoUtils.asLocalDateTime(p.getProjectedCompletion())) @@ -141,10 +143,11 @@ void inserttaskTaskedOrganizations(@Bind("taskUuid") String taskUuid, public int updateInternal(Task p) { return getDbHandle().createUpdate( "/* updateTask */ UPDATE tasks set \"longName\" = :longName, \"shortName\" = :shortName, " - + "category = :category, \"customFieldRef1Uuid\" = :customFieldRef1Uuid, \"updatedAt\" = :updatedAt, status = :status, " - + "\"customField\" = :customField, \"customFieldEnum1\" = :customFieldEnum1, \"customFieldEnum2\" = :customFieldEnum2, " + + "category = :category, \"customFieldRef1Uuid\" = :customFieldRef1Uuid, " + + "\"updatedAt\" = :updatedAt, status = :status, \"customField\" = :customField, " + + "\"customFieldEnum1\" = :customFieldEnum1, \"customFieldEnum2\" = :customFieldEnum2, " + "\"plannedCompletion\" = :plannedCompletion, \"projectedCompletion\" = :projectedCompletion, " - + "\"customFields\" = :customFields " + "WHERE uuid = :uuid") + + "\"customFields\" = :customFields WHERE uuid = :uuid") .bindBean(p).bind("updatedAt", DaoUtils.asLocalDateTime(p.getUpdatedAt())) .bind("plannedCompletion", DaoUtils.asLocalDateTime(p.getPlannedCompletion())) .bind("projectedCompletion", DaoUtils.asLocalDateTime(p.getProjectedCompletion())) diff --git a/src/main/java/mil/dds/anet/database/mappers/LocationMapper.java b/src/main/java/mil/dds/anet/database/mappers/LocationMapper.java index 4dcd5d6170..c411806996 100644 --- a/src/main/java/mil/dds/anet/database/mappers/LocationMapper.java +++ b/src/main/java/mil/dds/anet/database/mappers/LocationMapper.java @@ -11,7 +11,7 @@ public class LocationMapper implements RowMapper { @Override public Location map(ResultSet rs, StatementContext ctx) throws SQLException { Location l = new Location(); - MapperUtils.setCommonBeanFields(l, rs, null); + MapperUtils.setCustomizableBeanFields(l, rs, null); l.setName(rs.getString("name")); l.setStatus(MapperUtils.getEnumIdx(rs, "status", Location.Status.class)); // preserve NULL values; when NULL there are no coordinates set: diff --git a/src/main/java/mil/dds/anet/database/mappers/OrganizationMapper.java b/src/main/java/mil/dds/anet/database/mappers/OrganizationMapper.java index 7f0bf126ed..7aec23039c 100644 --- a/src/main/java/mil/dds/anet/database/mappers/OrganizationMapper.java +++ b/src/main/java/mil/dds/anet/database/mappers/OrganizationMapper.java @@ -12,7 +12,7 @@ public class OrganizationMapper implements RowMapper { @Override public Organization map(ResultSet r, StatementContext ctx) throws SQLException { Organization org = new Organization(); - MapperUtils.setCommonBeanFields(org, r, "organizations"); + MapperUtils.setCustomizableBeanFields(org, r, "organizations"); org.setShortName(r.getString("organizations_shortName")); org.setLongName(r.getString("organizations_longName")); org.setStatus(MapperUtils.getEnumIdx(r, "organizations_status", Organization.Status.class)); diff --git a/src/main/java/mil/dds/anet/database/mappers/PositionMapper.java b/src/main/java/mil/dds/anet/database/mappers/PositionMapper.java index 7159faa0be..d52d03f69d 100644 --- a/src/main/java/mil/dds/anet/database/mappers/PositionMapper.java +++ b/src/main/java/mil/dds/anet/database/mappers/PositionMapper.java @@ -25,7 +25,7 @@ public Position map(ResultSet rs, StatementContext ctx) throws SQLException { } public static Position fillInFields(Position p, ResultSet rs) throws SQLException { - MapperUtils.setCommonBeanFields(p, rs, "positions"); + MapperUtils.setCustomizableBeanFields(p, rs, "positions"); p.setName(rs.getString("positions_name")); p.setCode(rs.getString("positions_code")); p.setType(MapperUtils.getEnumIdx(rs, "positions_type", PositionType.class)); diff --git a/src/main/java/mil/dds/anet/resources/LocationResource.java b/src/main/java/mil/dds/anet/resources/LocationResource.java index e5809132e5..5ff1f29568 100644 --- a/src/main/java/mil/dds/anet/resources/LocationResource.java +++ b/src/main/java/mil/dds/anet/resources/LocationResource.java @@ -49,6 +49,7 @@ public AnetBeanList search(@GraphQLRootContext Map con @GraphQLMutation(name = "createLocation") public Location createLocation(@GraphQLRootContext Map context, @GraphQLArgument(name = "location") Location l) { + l.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); AuthUtils.assertSuperUser(user); if (l.getName() == null || l.getName().trim().length() == 0) { @@ -79,6 +80,7 @@ public Location createLocation(@GraphQLRootContext Map context, @GraphQLMutation(name = "updateLocation") public Integer updateLocation(@GraphQLRootContext Map context, @GraphQLArgument(name = "location") Location l) { + l.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); AuthUtils.assertSuperUser(user); final int numRows = dao.update(l); diff --git a/src/main/java/mil/dds/anet/resources/OrganizationResource.java b/src/main/java/mil/dds/anet/resources/OrganizationResource.java index d91f6bf2c1..1023354280 100644 --- a/src/main/java/mil/dds/anet/resources/OrganizationResource.java +++ b/src/main/java/mil/dds/anet/resources/OrganizationResource.java @@ -51,6 +51,7 @@ public Organization getByUuid(@GraphQLArgument(name = "uuid") String uuid) { @GraphQLMutation(name = "createOrganization") public Organization createOrganization(@GraphQLRootContext Map context, @GraphQLArgument(name = "organization") Organization org) { + org.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); AuthUtils.assertAdministrator(user); final Organization created; @@ -91,6 +92,7 @@ public Organization createOrganization(@GraphQLRootContext Map c @GraphQLMutation(name = "updateOrganization") public Integer updateOrganization(@GraphQLRootContext Map context, @GraphQLArgument(name = "organization") Organization org) { + org.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); // Verify correct Organization AuthUtils.assertSuperUserForOrg(user, DaoUtils.getUuid(org), false); diff --git a/src/main/java/mil/dds/anet/resources/PositionResource.java b/src/main/java/mil/dds/anet/resources/PositionResource.java index cfb6114ea5..057d031311 100644 --- a/src/main/java/mil/dds/anet/resources/PositionResource.java +++ b/src/main/java/mil/dds/anet/resources/PositionResource.java @@ -70,6 +70,7 @@ private void assertCanUpdatePosition(Person user, Position pos) { @GraphQLMutation(name = "createPosition") public Position createPosition(@GraphQLRootContext Map context, @GraphQLArgument(name = "position") Position pos) { + pos.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); assertCanUpdatePosition(user, pos); validatePosition(user, pos); @@ -114,6 +115,7 @@ public Integer updateAssociatedPosition(@GraphQLRootContext Map @GraphQLMutation(name = "updatePosition") public Integer updatePosition(@GraphQLRootContext Map context, @GraphQLArgument(name = "position") Position pos) { + pos.checkAndFixCustomFields(); final Person user = DaoUtils.getUserFromContext(context); assertCanUpdatePosition(user, pos); validatePosition(user, pos); diff --git a/src/main/java/mil/dds/anet/views/AbstractCustomizableAnetBean.java b/src/main/java/mil/dds/anet/views/AbstractCustomizableAnetBean.java index 6977f96fce..e5f3fef5b8 100644 --- a/src/main/java/mil/dds/anet/views/AbstractCustomizableAnetBean.java +++ b/src/main/java/mil/dds/anet/views/AbstractCustomizableAnetBean.java @@ -4,6 +4,7 @@ import io.leangen.graphql.annotations.GraphQLInputField; import io.leangen.graphql.annotations.GraphQLQuery; import java.lang.invoke.MethodHandles; +import java.util.Objects; import mil.dds.anet.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +16,7 @@ public abstract class AbstractCustomizableAnetBean extends AbstractAnetBean { @GraphQLQuery @GraphQLInputField - protected String customFields; + private String customFields; public String getCustomFields() { return customFields; @@ -32,7 +33,20 @@ public void checkAndFixCustomFields() { setCustomFields(null); logger.error("Unable to process Json, customFields payload discarded", e); } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AbstractCustomizableAnetBean)) { + return false; + } + final AbstractCustomizableAnetBean other = (AbstractCustomizableAnetBean) o; + return Objects.equals(customFields, other.getCustomFields()); + } + @Override + public int hashCode() { + return Objects.hash(customFields); } } diff --git a/src/main/resources/anet-schema.yml b/src/main/resources/anet-schema.yml index e3285d633e..63e69de990 100644 --- a/src/main/resources/anet-schema.yml +++ b/src/main/resources/anet-schema.yml @@ -523,6 +523,10 @@ properties: enum: [LAT_LON, MGRS] title: Coordinate format for location description: Used in the UI where a location's coordinate is shown. Defaults to LAT_LON. + customFields: + type: object + additionalProperties: + "$ref": "#/$defs/customField" position: type: object @@ -533,6 +537,10 @@ properties: type: string title: The label for a position's name description: Used in the UI where a position's name is shown. + customFields: + type: object + additionalProperties: + "$ref": "#/$defs/customField" organization: type: object @@ -547,6 +555,10 @@ properties: type: string title: The label for an organization's parent organization description: Used in the UI where an organization's parent organization is shown. + customFields: + type: object + additionalProperties: + "$ref": "#/$defs/customField" advisor: type: object diff --git a/src/main/resources/migrations.xml b/src/main/resources/migrations.xml index 3bd72d8b1b..10b72f2c91 100644 --- a/src/main/resources/migrations.xml +++ b/src/main/resources/migrations.xml @@ -3640,4 +3640,19 @@ + + + + + + + + + + + + + + +