From 7a9a2d0456c58db59a1480aebe328fd6a007fe74 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 08:29:35 +0200 Subject: [PATCH 01/10] WIP implement min max name generator nodes --- network-canvas | 2 +- src/components/Form/ValidatedField.js | 4 +- src/components/StageEditor/Interfaces.js | 4 + src/components/sections/MinMaxAlterLimits.js | 117 +++++++++++++++++++ src/components/sections/index.js | 1 + src/protocol-validation | 2 +- src/utils/validations.js | 9 ++ 7 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 src/components/sections/MinMaxAlterLimits.js diff --git a/network-canvas b/network-canvas index fe649d2db..e51a7157f 160000 --- a/network-canvas +++ b/network-canvas @@ -1 +1 @@ -Subproject commit fe649d2db723e51fccfcd1adf1635900e62c5a17 +Subproject commit e51a7157f5393d64062edcc039228372c68a24a1 diff --git a/src/components/Form/ValidatedField.js b/src/components/Form/ValidatedField.js index 93e8d364a..6808e9b6a 100644 --- a/src/components/Form/ValidatedField.js +++ b/src/components/Form/ValidatedField.js @@ -7,12 +7,12 @@ const ValidatedField = ({ validation, ...rest }) => { - const validate = useValidate(validation); + const validations = useValidate(validation); return ( ); }; diff --git a/src/components/StageEditor/Interfaces.js b/src/components/StageEditor/Interfaces.js index d8a6cb9fa..f18af433f 100644 --- a/src/components/StageEditor/Interfaces.js +++ b/src/components/StageEditor/Interfaces.js @@ -18,6 +18,7 @@ import { NarrativePresets, NodePanels, NodeType, + MinMaxAlterLimits, OrdinalBinPrompts, QuickAdd, SearchOptionsForExternalData, @@ -101,6 +102,7 @@ const NameGenerator = { NameGeneratorPrompts, NodePanels, SkipLogic, + MinMaxAlterLimits, InterviewScript, ], documentation: 'https://documentation.networkcanvas.com/interface-documentation/name-generator-using-forms/', @@ -116,6 +118,7 @@ const NameGeneratorRoster = { SearchOptionsForExternalData, NameGeneratorRosterPrompts, SkipLogic, + MinMaxAlterLimits, InterviewScript, ], documentation: 'https://documentation.networkcanvas.com/interface-documentation/name-generator-roster/', @@ -129,6 +132,7 @@ const NameGeneratorQuickAdd = { NameGeneratorPrompts, NodePanels, SkipLogic, + MinMaxAlterLimits, InterviewScript, ], name: 'Name Generator (quick add)', diff --git a/src/components/sections/MinMaxAlterLimits.js b/src/components/sections/MinMaxAlterLimits.js new file mode 100644 index 000000000..7b7be69aa --- /dev/null +++ b/src/components/sections/MinMaxAlterLimits.js @@ -0,0 +1,117 @@ +import React, { useCallback, useMemo } from 'react'; +import { get, isNull } from 'lodash'; +import { Section } from '@components/EditorLayout'; +import { + change, FormSection, formValueSelector, +} from 'redux-form'; +import { actionCreators as dialogActions } from '@modules/dialogs'; +import { useDispatch, useSelector } from 'react-redux'; +import { Number } from '@codaco/ui/lib/components/Fields'; +import { ValidatedField } from '../Form'; +import IssueAnchor from '../IssueAnchor'; + +const maxValidation = (value, allValues) => { + const minValue = get(allValues, 'behaviours.minNodes', null); + + if (isNull(minValue) || !value) { + return undefined; + } + + return ( + value >= minValue ? undefined : 'Maximum number of alters must be greater than or equal to the minimum number' + ); +}; + +const minValidation = (value, allValues) => { + const maxValue = get(allValues, 'behaviours.maxNodes'); + + if (isNull(maxValue) || !value) { + return undefined; + } + + return ( + value <= maxValue ? undefined : 'Minimum number of alters must be less than or equal to the maximum number' + ); +}; + +const MinMaxAlterLimits = () => { + const getFormValue = formValueSelector('edit-stage'); + const currentMinValue = useSelector((state) => getFormValue(state, 'behaviours.minNodes')); + const currentMaxValue = useSelector((state) => getFormValue(state, 'behaviours.maxNodes')); + + const dispatch = useDispatch(); + const openDialog = useCallback( + (dialog) => dispatch(dialogActions.openDialog(dialog)), + [dispatch], + ); + + const handleToggleChange = useCallback( + async (newState) => { + console.log('handle') + if ((isNull(currentMinValue) && isNull(currentMaxValue)) || newState === true) { + return true; + } + + const confirm = await openDialog({ + type: 'Warning', + title: 'This will clear your values', + message: 'This will clear the minimum and maximum alter values. Do you want to continue?', + confirmLabel: 'Clear values', + }); + + if (confirm) { + dispatch(change('edit-stage', 'behaviours.minNodes', null)); + dispatch(change('edit-stage', 'behaviours.maxNodes', null)); + return true; + } + + return false; + }, + [dispatch, openDialog, currentMinValue, currentMaxValue], + ); + + const startExpanded = useMemo(() => !isNull(currentMinValue) || !isNull(currentMaxValue), []); + + return ( +
+ This feature allows you to specify that a minimum or maximum number of alters be named. + These limits apply to the stage as a whole, regardless of the number of + prompts you have. +

+ )} + toggleable + startExpanded={startExpanded} + handleToggleChange={handleToggleChange} + > + + + + + + +
+ ); +}; + +export default MinMaxAlterLimits; diff --git a/src/components/sections/index.js b/src/components/sections/index.js index 1dc9324a2..eaf808e92 100644 --- a/src/components/sections/index.js +++ b/src/components/sections/index.js @@ -16,6 +16,7 @@ export { default as NarrativeBehaviours } from './NarrativeBehaviours'; export { default as NarrativePresets } from './NarrativePresets'; export { default as NodePanels } from './NodePanels'; export { default as NodeType } from './NodeType'; +export { default as MinMaxAlterLimits } from './MinMaxAlterLimits'; export { default as OrdinalBinPrompts } from './OrdinalBinPrompts'; export { default as QuickAdd } from './QuickAdd'; export { default as SearchOptionsForExternalData } from './SearchOptionsForExternalData'; diff --git a/src/protocol-validation b/src/protocol-validation index cb76e48d0..af149cb41 160000 --- a/src/protocol-validation +++ b/src/protocol-validation @@ -1 +1 @@ -Subproject commit cb76e48d07fe24dced6d78d9d964c05aaff4df77 +Subproject commit af149cb4177142f85ab89b589453bfa555e5ed62 diff --git a/src/utils/validations.js b/src/utils/validations.js index 30433ea17..12d779434 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -137,6 +137,15 @@ const validations = { /** * Returns the named validation function, if no matching one is found it returns a validation * which will always fail. +* +* Use with a custom function by passing a function as the object value: +* +* const customValidation = (value) => { +* return value === 'custom' ? undefined : 'Custom validation failed'; +* }; +* +* validations.custom = customValidation; +* * @param {object} validationOptions The protocol config for the validations to return. */ export const getValidations = (validationOptions = {}) => map( From 2d8190db3187d2ed610f7d7f5985b377c1ac1538 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 10:17:48 +0200 Subject: [PATCH 02/10] add validation tests and attempt to reconcile NC and Architect validation functions --- .../Fields/VariablePicker/VariablePill.js | 4 +- src/components/sections/MinMaxAlterLimits.js | 14 +- src/utils/__tests__/validations.test.js | 196 ++++++++++++++++++ src/utils/validations.js | 54 ++--- 4 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 src/utils/__tests__/validations.test.js diff --git a/src/components/Form/Fields/VariablePicker/VariablePill.js b/src/components/Form/Fields/VariablePicker/VariablePill.js index 876e90807..d01d051c3 100644 --- a/src/components/Form/Fields/VariablePicker/VariablePill.js +++ b/src/components/Form/Fields/VariablePicker/VariablePill.js @@ -12,7 +12,7 @@ import { get } from 'lodash'; import cx from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { actionCreators as codebookActions } from '@modules/protocol/codebook'; -import { requiredWithMessage, uniqueByList, allowedVariableName } from '@app/utils/validations'; +import { required as requiredValidation, uniqueByList, allowedVariableName } from '@app/utils/validations'; import TextInput from '@codaco/ui/lib/components/Fields/Text'; import { makeGetVariableFromUUID, getVariablesForSubject } from '../../../../selectors/codebook'; import { getColorForType, getIconForType } from '../../../../config/variables'; @@ -108,7 +108,7 @@ const EditableVariablePill = ({ uuid }) => { const { target: { value } } = event; setNewName(value); - const required = requiredWithMessage('You must enter a variable name', true)(value); + const required = requiredValidation(true, 'You must enter a variable name', true)(value); const unique = uniqueByList(existingVariableNames)(value); const allowed = allowedVariableName()(value); diff --git a/src/components/sections/MinMaxAlterLimits.js b/src/components/sections/MinMaxAlterLimits.js index 7b7be69aa..20cb4c444 100644 --- a/src/components/sections/MinMaxAlterLimits.js +++ b/src/components/sections/MinMaxAlterLimits.js @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react'; -import { get, isNull } from 'lodash'; +import { get, isNull, isUndefined } from 'lodash'; import { Section } from '@components/EditorLayout'; import { change, FormSection, formValueSelector, @@ -13,7 +13,7 @@ import IssueAnchor from '../IssueAnchor'; const maxValidation = (value, allValues) => { const minValue = get(allValues, 'behaviours.minNodes', null); - if (isNull(minValue) || !value) { + if (isUndefined(minValue) || isNull(minValue) || !value) { return undefined; } @@ -25,7 +25,7 @@ const maxValidation = (value, allValues) => { const minValidation = (value, allValues) => { const maxValue = get(allValues, 'behaviours.maxNodes'); - if (isNull(maxValue) || !value) { + if (isUndefined(maxValue) || isNull(maxValue) || !value) { return undefined; } @@ -47,8 +47,7 @@ const MinMaxAlterLimits = () => { const handleToggleChange = useCallback( async (newState) => { - console.log('handle') - if ((isNull(currentMinValue) && isNull(currentMaxValue)) || newState === true) { + if ((isUndefined(currentMinValue) && isUndefined(currentMaxValue)) || newState === true) { return true; } @@ -70,7 +69,8 @@ const MinMaxAlterLimits = () => { [dispatch, openDialog, currentMinValue, currentMaxValue], ); - const startExpanded = useMemo(() => !isNull(currentMinValue) || !isNull(currentMaxValue), []); + const startExpanded = useMemo(() => !isUndefined(currentMinValue) + || !isUndefined(currentMaxValue), []); return (
{ placeholder="Infinity" validation={{ greaterThanMin: maxValidation, - positiveNumber: true, + minValue: 1, }} /> diff --git a/src/utils/__tests__/validations.test.js b/src/utils/__tests__/validations.test.js new file mode 100644 index 000000000..c05a20da0 --- /dev/null +++ b/src/utils/__tests__/validations.test.js @@ -0,0 +1,196 @@ +/* eslint-env jest */ + +import { + required, + minLength, + maxLength, + minValue, + maxValue, + minSelected, + maxSelected, +} from '../validations'; + +describe('Validations', () => { + describe('required()', () => { + const errorMessage = 'You must answer this question before continuing'; + const subject = required(true, errorMessage); + + it('passes for a string', () => { + expect(subject('hello world')).toBe(undefined); + }); + + it('passes for a numerical value', () => { + expect(subject(3)).toBe(undefined); + expect(subject(0)).toBe(undefined); + }); + + it('fails for null or undefined', () => { + expect(subject(null)).toEqual(errorMessage); + expect(subject(undefined)).toEqual(errorMessage); + }); + + it('fails for an empty string', () => { + expect(subject('')).toEqual(errorMessage); + }); + }); + + describe('minLength()', () => { + const errorMessage = 'Your answer must be 5 characters or more'; + const subject = minLength(5, errorMessage); + + it('fails for null or undefined', () => { + expect(subject(null)).toBe(errorMessage); + expect(subject(undefined)).toBe(errorMessage); + }); + + it('fails for a smaller string', () => { + expect(subject('hi')).toBe(errorMessage); + }); + + it('passes for an exactly matching string', () => { + expect(subject('hello')).toBe(undefined); + }); + + it('passes for a larger string', () => { + expect(subject('hello world')).toBe(undefined); + }); + }); + + describe('maxLength()', () => { + const errorMessage = 'Your answer must be 5 characters or less'; + const subject = maxLength(5, errorMessage); + + it('passes for null or undefined', () => { + expect(subject(null)).toBe(undefined); + expect(subject(undefined)).toBe(undefined); + }); + + it('passes for a smaller string', () => { + expect(subject('hi')).toBe(undefined); + }); + + it('passes for an exactly matching string', () => { + expect(subject('hello')).toBe(undefined); + }); + + it('fails for a larger string', () => { + expect(subject('hello world')).toBe(errorMessage); + }); + }); + + describe('minValue()', () => { + const errorMessage = 'Your answer must be at least 5'; + const subject = minValue(5, errorMessage); + + it('passes for null or undefined', () => { + expect(subject(null)).toBe(undefined); + expect(subject(undefined)).toBe(undefined); + }); + + it('fails for a negative number', () => { + expect(subject(-1)).toBe(errorMessage); + }); + + it('fails for 0', () => { + expect(subject(0)).toBe(errorMessage); + }); + + it('fails for a smaller value', () => { + expect(subject(3)).toBe(errorMessage); + }); + + it('passes for an exactly matching value', () => { + expect(subject(5)).toBe(undefined); + }); + + it('passes for a larger value', () => { + expect(subject(10)).toBe(undefined); + }); + }); + + describe('maxValue()', () => { + const errorMessage = 'Your answer must be less than 5'; + const subject = maxValue(5, errorMessage); + + it('passes for null or undefined', () => { + expect(subject(null)).toBe(undefined); + expect(subject(undefined)).toBe(undefined); + }); + + it('passes for a negative number', () => { + expect(subject(-1)).toBe(undefined); + }); + + it('passes for 0', () => { + expect(subject(0)).toBe(undefined); + }); + + it('passes for a smaller value', () => { + expect(subject(3)).toBe(undefined); + }); + + it('passes for an exactly matching value', () => { + expect(subject(5)).toBe(undefined); + }); + + it('fails for a larger value', () => { + expect(subject(10)).toBe(errorMessage); + }); + }); + + describe('minSelected()', () => { + const errorMessage = 'You must choose a minimum of 2 option(s)'; + const subject = minSelected(2, errorMessage); + + it('fails for null or undefined', () => { + expect(subject(null)).toBe(errorMessage); + expect(subject(undefined)).toBe(errorMessage); + }); + + it('fails for an empty array', () => { + expect(subject([])).toBe(errorMessage); + }); + + it('fails for a smaller array', () => { + expect(subject([1])).toBe(errorMessage); + }); + + it('passes for an exactly matching array', () => { + expect(subject([1, 2])).toBe(undefined); + }); + + it('passes for a larger array', () => { + expect(subject([1, 2, 3])).toBe(undefined); + }); + }); + + describe('maxSelected()', () => { + const errorMessage = 'You must choose a maximum of 2 option(s)'; + const subject = maxSelected(2, errorMessage); + + it('passes for null or undefined', () => { + expect(subject(null)).toBe(undefined); + expect(subject(undefined)).toBe(undefined); + }); + + it('passes for an empty array', () => { + expect(subject([])).toBe(undefined); + }); + + it('passes for a smaller array', () => { + expect(subject([1])).toBe(undefined); + }); + + it('correctly handles zero values', () => { + expect(subject([0, false, -1])).toBe(errorMessage); + }); + + it('passes for an exactly matching array', () => { + expect(subject([1, 2])).toBe(undefined); + }); + + it('fails for a larger array', () => { + expect(subject([1, 2, 3])).toBe(errorMessage); + }); + }); +}); diff --git a/src/utils/validations.js b/src/utils/validations.js index 12d779434..a16e05427 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -1,11 +1,22 @@ import { - keys, get, pickBy, map, toPairs, isEqual, isUndefined, isNil, + get, isEmpty, map, toPairs, isEqual, isUndefined, isNil, isNull, } from 'lodash'; import { DateTime } from 'luxon'; +// Simple function to allow returning a custom message if provided, and +// or defaulting to the default message. +const messageWithDefault = (message, defaultMessage) => { + if (message) { + return message; + } + return defaultMessage; +}; + +// Return an array of values given either a collection, an array, +// or a single value const coerceArray = (value) => { if (value instanceof Object) { - return keys(pickBy(value)); + return value.reduce((acc, individual) => ([...acc, individual.value]), []); } if (value instanceof Array) { return value; @@ -31,35 +42,30 @@ const isRoughlyEqual = (left, right) => { return isEqual(left, right); }; -export const required = (isRequired) => (value) => { +export const required = (isRequired, message) => (value) => { if (!isRequired) { return undefined; } - return (hasValue(value) ? undefined : 'Required'); + return (hasValue(value) ? undefined : messageWithDefault(message, 'Required')); }; -export const requiredWithMessage = ( - message, - isRequired, -) => (value) => (required(isRequired)(value) ? message : undefined); - -export const requiredAcceptsZero = () => (value) => (!isNil(value) ? undefined : 'Required'); +export const requiredAcceptsZero = (isRequired, message) => (value) => (!isNil(value) && isRequired ? undefined : messageWithDefault(message, 'Required')); -export const requiredAcceptsNull = () => (value) => (!isUndefined(value) ? undefined : 'Required'); +export const requiredAcceptsNull = (isRequired, message) => (value) => (!isUndefined(value) && isRequired ? undefined : messageWithDefault(message, 'Required')); -export const positiveNumber = () => (value) => (value && Math.sign(value) === -1 ? 'Number must be positive' : undefined); +export const positiveNumber = (message) => (value) => (value && Math.sign(value) === -1 ? messageWithDefault(message, 'Number must be positive') : undefined); -export const maxLength = (max) => (value) => (value && value.length > max ? `Must be ${max} characters or less` : undefined); -export const minLength = (min) => (value) => (value && value.length < min ? `Must be ${min} characters or more` : undefined); -export const minValue = (min) => (value) => (value && value < min ? `Must be at least ${min}` : undefined); -export const maxValue = (max) => (value) => (value && value > max ? `Must be less than ${max}` : undefined); +export const maxLength = (max, message) => (value) => (!isNull(value) && !isUndefined(value) && value.length > max ? messageWithDefault(message, `Must be ${max} characters or less`) : undefined); +export const minLength = (min, message) => (value) => ((isNull(value) || isUndefined(value)) || value.length < min ? messageWithDefault(message, `Must be ${min} characters or more`) : undefined); -export const minSelected = (min) => (value) => (!value || coerceArray(value).length < min ? `Must choose ${min} or more` : undefined); +export const minValue = (min, message) => (value) => (!isNull(value) && value < min ? messageWithDefault(message, `Must be at least ${min}`) : undefined); +export const maxValue = (max, message) => (value) => (value && value > max ? messageWithDefault(message, `Must be less than ${max}`) : undefined); -export const maxSelected = (max) => (value) => (!value || coerceArray(value).length > max ? `Must choose ${max} or less` : undefined); +export const minSelected = (min, message) => (value) => (!value || coerceArray(value).length < min ? messageWithDefault(message, `You must choose a minimum of ${min} option(s)`) : undefined); +export const maxSelected = (max, message) => (value) => (!isEmpty(value) && coerceArray(value).length > max ? messageWithDefault(message, `You must choose a maximum of ${max} option(s)`) : undefined); -export const uniqueArrayAttribute = () => (value, allValues, formProps, name) => { +export const uniqueArrayAttribute = (message) => (value, allValues, name) => { if (!value) { return undefined; } // expects `name` of format: `fieldName[n].attribute` @@ -76,31 +82,31 @@ export const uniqueArrayAttribute = () => (value, allValues, formProps, name) => }, 0); if (instanceCount >= 2) { - return `${capitalize(attribute)}s must be unique`; + return messageWithDefault(message, `${capitalize(attribute)}s must be unique`); } return undefined; }; -export const uniqueByList = (list = []) => (value) => { +export const uniqueByList = (list = [], message) => (value) => { if (!value) { return undefined; } const existsAlready = list .some((existingValue) => isRoughlyEqual(existingValue, value)); if (existsAlready) { - return `"${value}" is already used elsewhere in your protocol`; + return messageWithDefault(message, `"${value}" is already used elsewhere in your protocol`); } return undefined; }; -export const ISODate = (dateFormat) => (value) => { +export const ISODate = (dateFormat, message) => (value) => { const dt = DateTime.fromISO(value); if ( (value && dateFormat.length !== value.length) || (value && !dt.isValid) ) { - return `Date is not valid (${dateFormat.toUpperCase()})`; + return messageWithDefault(message, `Date is not valid (${dateFormat.toUpperCase()})`); } return undefined; }; From aff5b815dcc5ffe77f80a210663455a0c3166132 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 10:26:36 +0200 Subject: [PATCH 03/10] resolve missing parameter in uniqueArrayAttribute() --- .../CategoricalBinPrompts/__tests__/PromptFields.test.js | 2 +- src/utils/__tests__/validations.test.js | 8 ++++++++ src/utils/validations.js | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/sections/CategoricalBinPrompts/__tests__/PromptFields.test.js b/src/components/sections/CategoricalBinPrompts/__tests__/PromptFields.test.js index b1a92ac5f..e35b0044f 100644 --- a/src/components/sections/CategoricalBinPrompts/__tests__/PromptFields.test.js +++ b/src/components/sections/CategoricalBinPrompts/__tests__/PromptFields.test.js @@ -69,7 +69,7 @@ export const testPromptFields = (PromptFieldsComponent, name = '') => { mockStore = getStore(initialState); }); - // TODO This seems to test the wrong part of codebook + // FIXME This seems to test the wrong part of codebook describe(name, () => { describe('PromptFields', () => { diff --git a/src/utils/__tests__/validations.test.js b/src/utils/__tests__/validations.test.js index c05a20da0..47ce6d8b3 100644 --- a/src/utils/__tests__/validations.test.js +++ b/src/utils/__tests__/validations.test.js @@ -193,4 +193,12 @@ describe('Validations', () => { expect(subject([1, 2, 3])).toBe(errorMessage); }); }); + + it.todo('uniqueArrayAttributr()'); + + it.todo('uniqueByList()'); + + it.todo('ISODate()'); + + it.todo('allowedVariableName()'); }); diff --git a/src/utils/validations.js b/src/utils/validations.js index a16e05427..70c2133fa 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -65,7 +65,7 @@ export const maxValue = (max, message) => (value) => (value && value > max ? mes export const minSelected = (min, message) => (value) => (!value || coerceArray(value).length < min ? messageWithDefault(message, `You must choose a minimum of ${min} option(s)`) : undefined); export const maxSelected = (max, message) => (value) => (!isEmpty(value) && coerceArray(value).length > max ? messageWithDefault(message, `You must choose a maximum of ${max} option(s)`) : undefined); -export const uniqueArrayAttribute = (message) => (value, allValues, name) => { +export const uniqueArrayAttribute = (message) => (value, allValues, _, name) => { if (!value) { return undefined; } // expects `name` of format: `fieldName[n].attribute` From 59e2cff76245e93888b2fc7ec38684005e0e2a38 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 10:29:52 +0200 Subject: [PATCH 04/10] final wording change --- src/components/sections/MinMaxAlterLimits.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/sections/MinMaxAlterLimits.js b/src/components/sections/MinMaxAlterLimits.js index 20cb4c444..8e1e3cad6 100644 --- a/src/components/sections/MinMaxAlterLimits.js +++ b/src/components/sections/MinMaxAlterLimits.js @@ -74,11 +74,11 @@ const MinMaxAlterLimits = () => { return (
- This feature allows you to specify that a minimum or maximum number of alters be named. - These limits apply to the stage as a whole, regardless of the number of + This feature allows you to specify that a minimum or maximum number of alters that can + be named. These limits apply to the stage as a whole, regardless of the number of prompts you have.

)} From 1e45ee6ce34e6270d2ec55e6e2865dac409a8001 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 10:54:22 +0200 Subject: [PATCH 05/10] new interviewer ref --- network-canvas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-canvas b/network-canvas index e51a7157f..e5c2adb64 160000 --- a/network-canvas +++ b/network-canvas @@ -1 +1 @@ -Subproject commit e51a7157f5393d64062edcc039228372c68a24a1 +Subproject commit e5c2adb640c8ac5ee711fa6198037a8f2f2bcd88 From dd299e9740503a2f40d11d7b7f979d118f3724e2 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 10:54:49 +0200 Subject: [PATCH 06/10] bump version to 6.4.0 --- package-lock.json | 2 +- package.json | 2 +- public/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea8c4ba92..c261bf1df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "network-canvas-architect", - "version": "6.3.1", + "version": "6.4.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index d27ae52f4..3886f001a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-architect", - "version": "6.3.1", + "version": "6.4.0", "productName": "Network Canvas Architect", "description": "A tool for building Network Canvas interviews.", "author": "Complex Data Collective ", diff --git a/public/package.json b/public/package.json index ed53844e1..bdd41d873 100644 --- a/public/package.json +++ b/public/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-architect", - "version": "6.3.1", + "version": "6.4.0", "productName": "Network Canvas Architect", "description": "A tool for building Network Canvas interviews.", "author": "Complex Data Collective", From 84a3c66fd734be1bf1fde905d439fd0e36df8b72 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Thu, 17 Mar 2022 12:49:15 +0200 Subject: [PATCH 07/10] update interviewer reference --- network-canvas | 2 +- src/__mocks__/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/network-canvas b/network-canvas index e5c2adb64..a6127ae4d 160000 --- a/network-canvas +++ b/network-canvas @@ -1 +1 @@ -Subproject commit e5c2adb640c8ac5ee711fa6198037a8f2f2bcd88 +Subproject commit a6127ae4d584153f36500f1d3cd01679fe036496 diff --git a/src/__mocks__/config.js b/src/__mocks__/config.js index 193187e81..dfb266b91 100644 --- a/src/__mocks__/config.js +++ b/src/__mocks__/config.js @@ -1,4 +1,4 @@ /* eslint-env jest */ /* eslint-disable import/prefer-default-export */ -export const APP_SCHEMA_VERSION = 2; +export const APP_SCHEMA_VERSION = 7; From ab8a15bd1e9330e167e4620c7e7fb1626abc01ad Mon Sep 17 00:00:00 2001 From: Rebecca Madsen Date: Fri, 18 Mar 2022 21:11:47 -0400 Subject: [PATCH 08/10] fix params for validations --- src/utils/validations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/validations.js b/src/utils/validations.js index 70c2133fa..ab0de37d8 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -54,7 +54,7 @@ export const requiredAcceptsZero = (isRequired, message) => (value) => (!isNil(v export const requiredAcceptsNull = (isRequired, message) => (value) => (!isUndefined(value) && isRequired ? undefined : messageWithDefault(message, 'Required')); -export const positiveNumber = (message) => (value) => (value && Math.sign(value) === -1 ? messageWithDefault(message, 'Number must be positive') : undefined); +export const positiveNumber = (_, message) => (value) => (value && Math.sign(value) === -1 ? messageWithDefault(message, 'Number must be positive') : undefined); export const maxLength = (max, message) => (value) => (!isNull(value) && !isUndefined(value) && value.length > max ? messageWithDefault(message, `Must be ${max} characters or less`) : undefined); export const minLength = (min, message) => (value) => ((isNull(value) || isUndefined(value)) || value.length < min ? messageWithDefault(message, `Must be ${min} characters or more`) : undefined); @@ -65,7 +65,7 @@ export const maxValue = (max, message) => (value) => (value && value > max ? mes export const minSelected = (min, message) => (value) => (!value || coerceArray(value).length < min ? messageWithDefault(message, `You must choose a minimum of ${min} option(s)`) : undefined); export const maxSelected = (max, message) => (value) => (!isEmpty(value) && coerceArray(value).length > max ? messageWithDefault(message, `You must choose a maximum of ${max} option(s)`) : undefined); -export const uniqueArrayAttribute = (message) => (value, allValues, _, name) => { +export const uniqueArrayAttribute = (_, message) => (value, allValues, __, name) => { if (!value) { return undefined; } // expects `name` of format: `fieldName[n].attribute` From 8604e97352333e319d841142f28f41682be37b05 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Tue, 22 Mar 2022 18:52:55 +0200 Subject: [PATCH 09/10] add warning if there are multiple prompts on a stage --- package-lock.json | 2 +- src/components/sections/MinMaxAlterLimits.js | 37 +++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c261bf1df..3c0abb338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "network-canvas-architect", - "version": "6.3.0", + "version": "6.4.0", "hasInstallScript": true, "devDependencies": { "@babel/cli": "^7.12.1", diff --git a/src/components/sections/MinMaxAlterLimits.js b/src/components/sections/MinMaxAlterLimits.js index 8e1e3cad6..f9e20b474 100644 --- a/src/components/sections/MinMaxAlterLimits.js +++ b/src/components/sections/MinMaxAlterLimits.js @@ -9,6 +9,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Number } from '@codaco/ui/lib/components/Fields'; import { ValidatedField } from '../Form'; import IssueAnchor from '../IssueAnchor'; +import Tip from '../Tip'; const maxValidation = (value, allValues) => { const minValue = get(allValues, 'behaviours.minNodes', null); @@ -38,6 +39,10 @@ const MinMaxAlterLimits = () => { const getFormValue = formValueSelector('edit-stage'); const currentMinValue = useSelector((state) => getFormValue(state, 'behaviours.minNodes')); const currentMaxValue = useSelector((state) => getFormValue(state, 'behaviours.maxNodes')); + const hasMultiplePrompts = useSelector((state) => { + const prompts = getFormValue(state, 'prompts'); + return !!prompts && prompts.length > 1; + }); const dispatch = useDispatch(); const openDialog = useCallback( @@ -76,16 +81,38 @@ const MinMaxAlterLimits = () => {
- This feature allows you to specify that a minimum or maximum number of alters that can - be named. These limits apply to the stage as a whole, regardless of the number of - prompts you have. -

+ <> +

+ This feature allows you to specify a minimum or maximum number of alters that can + be named on this stage. Please note that these limits apply to the + {' '} + + stage as a whole + + , regardless of the number of + prompts you have created. +

+ )} toggleable startExpanded={startExpanded} handleToggleChange={handleToggleChange} > + { hasMultiplePrompts && ( + +

+ You have multiple prompts configured on this stage. Remember that the limits you + specify here apply to the + {' '} + + stage as a whole + + . Consider splitting your prompts up into multiple stages, or ensure you take extra + care in the phrasing of your prompts so that you communicate the alter limits + to your participants. +

+
+ )} Date: Tue, 22 Mar 2022 19:11:55 +0200 Subject: [PATCH 10/10] update submodule reference --- network-canvas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-canvas b/network-canvas index a6127ae4d..987da952d 160000 --- a/network-canvas +++ b/network-canvas @@ -1 +1 @@ -Subproject commit a6127ae4d584153f36500f1d3cd01679fe036496 +Subproject commit 987da952d9a3c8145d50b0c83c9f83fb64bd86f5