From a0d2b61149442d5713944dd25af365ce9498ab77 Mon Sep 17 00:00:00 2001 From: Farmer Paul Date: Tue, 10 Dec 2024 10:07:36 -0500 Subject: [PATCH 1/3] chore: add Greek translation for empty state (#559) --- src/i18n/el/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/el/translation.json b/src/i18n/el/translation.json index 24b81493..b5836498 100644 --- a/src/i18n/el/translation.json +++ b/src/i18n/el/translation.json @@ -329,6 +329,7 @@ "support": "Υποστήριξη", "privacy": "Απόρρητο", "termsOfService": "Όροι" - } + }, + "noActivities": "Δεν υπάρχουν διαθέσιμες δραστηριότητες για να ολοκληρώσετε αυτήν τη στιγμή" } } From 75ce9e80696a7aafbf9e3e00cf8d8bb429a0a3e1 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne <14842108+sultanofcardio@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:15:04 -0500 Subject: [PATCH 2/3] chore: Include extra data in call to JIRA webhook (#557) Whenever a release candidate deployment happens from this repository, a GitHub Actions workflow attempts to process the tickets it contains, and consumes a Jira webhook that triggers an automation to move those tickets into the "To Be Tested" column. I recently updated that automation to also leave a comment on the ticket when the deployment happens This PR adds more details to the webhook call so that the comment may include more details about the deployment. These details are: - The release candidate tag - The repository URL --- .github/workflows/update-jira-tickets.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-jira-tickets.yaml b/.github/workflows/update-jira-tickets.yaml index c7e57a76..03e030f8 100644 --- a/.github/workflows/update-jira-tickets.yaml +++ b/.github/workflows/update-jira-tickets.yaml @@ -103,6 +103,8 @@ jobs: echo "tickets=${jiraTickets}" >> $GITHUB_OUTPUT - name: Periodically ping Jenkins for current tag build status + env: + REPO_URL: "${{ github.server_url }}/${{ github.repository }}" run: | repoName=${GITHUB_REPOSITORY##*/} currentTag="${{ steps.get-tag.outputs.tag }}" @@ -124,7 +126,7 @@ jobs: if [[ "$result" == "SUCCESS" ]]; then echo "Build successful! Submitting ticket numbers to Jira" tickets="${{ steps.jira-tickets.outputs.tickets }}" - json="{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]') }" + json="{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]'), \"data\": { \"tag\": \"${currentTag}\", \"repository\": \"${REPO_URL}\" } }" curl -X POST -H 'Content-Type: application/json' --url "${JIRA_WEBHOOK_URL}" --data "$json" break elif [[ "$result" != "null" ]]; then From 5a99170fac5d99e191b20d718a72a92b3b768e0d Mon Sep 17 00:00:00 2001 From: Farmer Paul Date: Thu, 12 Dec 2024 12:04:16 -0500 Subject: [PATCH 3/3] fix: Action Plan matrix item type render bugs (M2-8348) (#560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: fix logic for generating matrix answers Simplified and fixed logic for generating response text for matrix item types (single selection per row, multiple selection per row). Omitted row selections should not display any "skipped" text. Also, the order of responses shouldn't change based on the format picked from the dropdown – responses should always be output by column left to right, then by row top to bottom. The only thing that changes based on the format selection is whether to output the response as "columnLabel rowLabel" vs. "rowLabel columnLabel". * fix: improve slider response output - Do not treat slider 0 value as "skipped" for `sliderRows` - Output individual `slider` response in same format as `sliderRows` --- .../ui/items/ActionPlan/pageComponent.ts | 76 +++++------ .../ui/items/ActionPlan/phrasalData.test.ts | 56 ++++----- .../ui/items/ActionPlan/phrasalData.ts | 119 ++++++++---------- 3 files changed, 107 insertions(+), 144 deletions(-) diff --git a/src/entities/activity/ui/items/ActionPlan/pageComponent.ts b/src/entities/activity/ui/items/ActionPlan/pageComponent.ts index f9dfefa1..fe3f2eb6 100644 --- a/src/entities/activity/ui/items/ActionPlan/pageComponent.ts +++ b/src/entities/activity/ui/items/ActionPlan/pageComponent.ts @@ -13,7 +13,11 @@ import { SentencePageComponent, TextItemResponsePageComponent, } from './Document.type'; -import { ActivitiesPhrasalData, ActivityPhrasalDataSliderRowContext } from './phrasalData'; +import { + ActivitiesPhrasalData, + ActivityPhrasalDataSliderContext, + ActivityPhrasalDataSliderRowContext, +} from './phrasalData'; const isAnswersSkipped = (answers: string[]): boolean => { if (!answers || answers.length <= 0) { @@ -32,13 +36,12 @@ const isAnswersSkipped = (answers: string[]): boolean => { const identity: FieldValueTransformer = (value) => value; const sliderValueTransformer = - ( - t: TFunction, - ctx: ActivityPhrasalDataSliderRowContext, - itemIndex: number, - ): FieldValueTransformer => + (t: TFunction, maxValue: number): FieldValueTransformer => (value) => - t('sliderValue', { value, total: ctx.maxValues[itemIndex] }); + t('sliderValue', { + value, + total: maxValue, + }); const joinWithComma: FieldValueItemsJoiner = (values) => values.join(', '); const joinWithDash: FieldValueItemsJoiner = (values) => values.join(' - '); @@ -79,11 +82,17 @@ export const buildPageComponents = ( if (fieldPhrasalData) { let transformValue = identity; let joinValueItems = joinWithComma; - if (fieldPhrasalData.context.itemResponseType === 'sliderRows') { + if (fieldPhrasalData.context.itemResponseType === 'slider') { + transformValue = sliderValueTransformer( + t, + (fieldPhrasalData.context as ActivityPhrasalDataSliderContext).maxValue, + ); + } else if (fieldPhrasalData.context.itemResponseType === 'sliderRows') { transformValue = sliderValueTransformer( t, - fieldPhrasalData.context as ActivityPhrasalDataSliderRowContext, - field.itemIndex, + (fieldPhrasalData.context as ActivityPhrasalDataSliderRowContext).maxValues[ + field.itemIndex + ], ); } else if (fieldPhrasalData.context.itemResponseType === 'timeRange') { joinValueItems = joinWithDash; @@ -100,44 +109,21 @@ export const buildPageComponents = ( ? [t('questionSkipped')] : indexedAnswers.map(transformValue); } else if (fieldPhrasalDataType === 'matrix') { - // The admin UI actually allows matrix type items to have `sentence` as - // their display mode. So in this case, we're just going to assume the - // effective render order for the values to be "by row". - let renderByRowValues = true; - if ( - field.displayMode === 'sentence_option_row' || - field.displayMode === 'bullet_list_option_row' - ) { - renderByRowValues = false; - } else if ( + const rowFirst = field.displayMode === 'sentence_row_option' || - field.displayMode === 'bullet_list_text_row' - ) { - renderByRowValues = true; + field.displayMode === 'bullet_list_text_row'; + + valueItems = []; + for (const { rowLabel, columnLabels } of fieldPhrasalData.values) { + for (const columnLabel of columnLabels) { + valueItems.push( + rowFirst ? `${rowLabel} ${columnLabel}` : `${columnLabel} ${rowLabel}`, + ); + } } - if (renderByRowValues) { - valueItems = fieldPhrasalData.values.byRow - .map(({ label, values }) => { - const transformedValues = isAnswersSkipped(values) - ? [t('questionSkipped')] - : values.map(transformValue); - return transformedValues.map( - (transformedValue) => `${label} ${transformedValue}`, - ); - }) - .flat(); - } else { - valueItems = fieldPhrasalData.values.byColumn - .map(({ label, values }) => { - const transformedValues = isAnswersSkipped(values) - ? [t('questionSkipped')] - : values.map(transformValue); - return transformedValues.map( - (transformedValue) => `${label} ${transformedValue}`, - ); - }) - .flat(); + if (isAnswersSkipped(valueItems)) { + valueItems = [t('questionSkipped')]; } } else { // This also shouldn't happen. But including a `else` here allows all diff --git a/src/entities/activity/ui/items/ActionPlan/phrasalData.test.ts b/src/entities/activity/ui/items/ActionPlan/phrasalData.test.ts index efaa93f0..494f309a 100644 --- a/src/entities/activity/ui/items/ActionPlan/phrasalData.test.ts +++ b/src/entities/activity/ui/items/ActionPlan/phrasalData.test.ts @@ -27,7 +27,6 @@ describe('Action Plan', () => { const newTimeItem = newSimpleItem('time'); const newTimeRangeItem = newSimpleItem('timeRange'); const newNumberSelectItem = newSimpleItem('numberSelect'); - const newSliderItem = newSimpleItem('slider'); const newTextItem = newSimpleItem('text'); const newSelectItem = (type: string) => (name: string, answer: string[], options: string[]) => @@ -53,14 +52,14 @@ describe('Action Plan', () => { responseType: 'multiSelectRows', responseValues: { rows: options[0].map((option) => ({ rowName: option })), - options: options[1].map((option) => ({ text: option })), + options: options[1].map((option, index) => ({ id: `col:${index}`, text: option })), }, answer, }) as never as ItemRecord; const newSingleSelectRowsItem = ( name: string, - answer: string[], + answer: (string | null)[], options: [string[], string[]], ) => ({ @@ -73,6 +72,14 @@ describe('Action Plan', () => { answer, }) as never as ItemRecord; + const newSliderItem = (name: string, answer: string[], maxValue: number) => + ({ + name, + responseType: 'slider', + responseValues: { maxValue }, + answer, + }) as never as ItemRecord; + const newSliderRowsItem = (name: string, answer: number[], options: [number, number][]) => ({ name, @@ -132,12 +139,13 @@ describe('Action Plan', () => { }); it('should extract data from `slider` activity type', () => { - const data = extractActivitiesPhrasalData([newSliderItem('item', ['6'])]); + const data = extractActivitiesPhrasalData([newSliderItem('item', ['6'], 10)]); expect(data).toHaveProperty('item'); expect(data.item).toHaveProperty('type', 'array'); expect(data.item).toHaveProperty('values.0', '6'); expect(data.item).toHaveProperty('context.itemResponseType', 'slider'); + expect(data.item).toHaveProperty('context.maxValue', 10); }); it('should extract data from `text` activity type', () => { @@ -199,22 +207,12 @@ describe('Action Plan', () => { expect(data).toHaveProperty('item'); expect(data.item).toHaveProperty('type', 'matrix'); - expect(data.item).toHaveProperty('values.byRow.0.label', 'R1'); - expect(data.item).toHaveProperty('values.byRow.0.values.0', 'C1'); - expect(data.item).toHaveProperty('values.byRow.0.values.1', 'C2'); - expect(data.item).toHaveProperty('values.byRow.1.label', 'R2'); - expect(data.item).toHaveProperty('values.byRow.1.values.0', 'C2'); - expect(data.item).toHaveProperty('values.byRow.2.label', 'R3'); - expect(data.item).toHaveProperty('values.byRow.2.values.0', 'C2'); - expect(data.item).toHaveProperty('values.byRow.2.values.1', 'C3'); - expect(data.item).toHaveProperty('values.byColumn.0.label', 'C1'); - expect(data.item).toHaveProperty('values.byColumn.0.values.0', 'R1'); - expect(data.item).toHaveProperty('values.byColumn.1.label', 'C2'); - expect(data.item).toHaveProperty('values.byColumn.1.values.0', 'R1'); - expect(data.item).toHaveProperty('values.byColumn.1.values.1', 'R2'); - expect(data.item).toHaveProperty('values.byColumn.1.values.2', 'R3'); - expect(data.item).toHaveProperty('values.byColumn.2.label', 'C3'); - expect(data.item).toHaveProperty('values.byColumn.2.values.0', 'R3'); + expect(data.item).toHaveProperty('values.0.rowLabel', 'R1'); + expect(data.item).toHaveProperty('values.0.columnLabels', ['C1', 'C2']); + expect(data.item).toHaveProperty('values.1.rowLabel', 'R2'); + expect(data.item).toHaveProperty('values.1.columnLabels', ['C2']); + expect(data.item).toHaveProperty('values.2.rowLabel', 'R3'); + expect(data.item).toHaveProperty('values.2.columnLabels', ['C2', 'C3']); expect(data.item).toHaveProperty('context.itemResponseType', 'multiSelectRows'); }); @@ -232,18 +230,12 @@ describe('Action Plan', () => { expect(data).toHaveProperty('item'); expect(data.item).toHaveProperty('type', 'matrix'); - expect(data.item).toHaveProperty('values.byRow.0.label', 'R1'); - expect(data.item).toHaveProperty('values.byRow.0.values.0', 'C3'); - expect(data.item).toHaveProperty('values.byRow.1.label', 'R2'); - expect(data.item).toHaveProperty('values.byRow.1.values.0', 'C1'); - expect(data.item).toHaveProperty('values.byRow.2.label', 'R3'); - expect(data.item).toHaveProperty('values.byRow.2.values.0', 'C2'); - expect(data.item).toHaveProperty('values.byColumn.0.label', 'C1'); - expect(data.item).toHaveProperty('values.byColumn.0.values.0', 'R2'); - expect(data.item).toHaveProperty('values.byColumn.1.label', 'C2'); - expect(data.item).toHaveProperty('values.byColumn.1.values.0', 'R3'); - expect(data.item).toHaveProperty('values.byColumn.2.label', 'C3'); - expect(data.item).toHaveProperty('values.byColumn.2.values.0', 'R1'); + expect(data.item).toHaveProperty('values.0.rowLabel', 'R1'); + expect(data.item).toHaveProperty('values.0.columnLabels', ['C3']); + expect(data.item).toHaveProperty('values.1.rowLabel', 'R2'); + expect(data.item).toHaveProperty('values.1.columnLabels', ['C1']); + expect(data.item).toHaveProperty('values.2.rowLabel', 'R3'); + expect(data.item).toHaveProperty('values.2.columnLabels', ['C2']); expect(data.item).toHaveProperty('context.itemResponseType', 'singleSelectRows'); }); diff --git a/src/entities/activity/ui/items/ActionPlan/phrasalData.ts b/src/entities/activity/ui/items/ActionPlan/phrasalData.ts index a61c4fb4..dd351846 100644 --- a/src/entities/activity/ui/items/ActionPlan/phrasalData.ts +++ b/src/entities/activity/ui/items/ActionPlan/phrasalData.ts @@ -8,6 +8,11 @@ type ActivityPhrasalDataGenericContext = { itemResponseType: ActivityItemType; }; +export type ActivityPhrasalDataSliderContext = ActivityPhrasalDataGenericContext & { + itemResponseType: 'slider'; + maxValue: number; +}; + export type ActivityPhrasalDataSliderRowContext = ActivityPhrasalDataGenericContext & { itemResponseType: 'sliderRows'; maxValues: number[]; @@ -33,14 +38,11 @@ type ActivityPhrasalIndexedArrayFieldData = ActivityPhrasalBaseData< >; type ActivityPhrasalIndexedMatrixValue = { - label: string; - values: string[]; + rowLabel: string; + columnLabels: string[]; }; -type ActivityPhrasalMatrixValue = { - byRow: ActivityPhrasalIndexedMatrixValue[]; - byColumn: ActivityPhrasalIndexedMatrixValue[]; -}; +type ActivityPhrasalMatrixValue = ActivityPhrasalIndexedMatrixValue[]; type ActivityPhrasalMatrixFieldData = ActivityPhrasalBaseData<'matrix', ActivityPhrasalMatrixValue>; @@ -85,13 +87,12 @@ export const extractActivitiesPhrasalData = (items: ItemRecord[]): ActivitiesPhr fieldData = timeFieldData; } else if ( item.responseType === 'numberSelect' || - item.responseType === 'slider' || item.responseType === 'text' || item.responseType === 'paragraphText' ) { const textFieldData: ActivityPhrasalArrayFieldData = { type: 'array', - values: item.answer.map((value) => `${value || ''}`), + values: item.answer.map((value) => `${value ?? ''}`), context: fieldDataContext, }; fieldData = textFieldData; @@ -104,70 +105,54 @@ export const extractActivitiesPhrasalData = (items: ItemRecord[]): ActivitiesPhr context: fieldDataContext, }; fieldData = selectFieldData; - } else if (item.responseType === 'multiSelectRows') { - const byRow = item.responseValues.rows.map( - (row, rowIndex) => { - return { - label: row.rowName, - values: - item.answer[rowIndex]?.filter( - (value): value is string => value !== null && value !== undefined, - ) || [], - }; - }, - ); - - const byColumn = item.responseValues.options.map( - (option) => { - const answerIndices: number[] = []; - item.answer.forEach((values, answerIndex) => { - if (values.find((value) => value === option.text)) { - answerIndices.push(answerIndex); - } + } else if ( + item.responseType === 'singleSelectRows' || + item.responseType === 'multiSelectRows' + ) { + // Unifiy logic for both singleSelectRows and multiSelectRows item types: + // - for singleSelectRows, map each answer to an array of one element + // - for multiSelectRows, answers are tracked as the _label_ of the column rather than ID, + // so we need to normalize to ID to align with singleSelectRows + const answers = + item.responseType === 'singleSelectRows' + ? item.answer.map((value) => [value]) + : item.answer.map((values) => + values.map( + (text) => item.responseValues.options.find((option) => option.text === text)?.id, + ), + ); + + const values: ActivityPhrasalIndexedMatrixValue[] = []; + + answers.forEach((optionIds, rowIndex) => { + const optionValues = item.responseValues.options + .filter(({ id }) => optionIds.includes(id)) + .map(({ text }) => text); + + if (optionValues.length) { + values.push({ + rowLabel: item.responseValues.rows[rowIndex].rowName, + columnLabels: optionValues, }); - return { - label: option.text, - values: answerIndices.map( - (answerIndex) => item.responseValues.rows[answerIndex].rowName || '', - ), - }; - }, - ); - - const selectFieldData: ActivityPhrasalMatrixFieldData = { + } + }); + + const matrixFieldData: ActivityPhrasalMatrixFieldData = { type: 'matrix', - values: { byRow, byColumn }, + values, context: fieldDataContext, }; - fieldData = selectFieldData; - } else if (item.responseType === 'singleSelectRows') { - const byRow = item.responseValues.rows.map( - (row, rowIndex) => { - return { - label: row.rowName, - values: [ - item.responseValues.options.find((option) => option.id === item.answer[rowIndex]) - ?.text || '', - ], - }; - }, - ); - - const byColumn = item.responseValues.options.map( - (option) => { - return { - label: option.text, - values: [item.responseValues.rows[item.answer.indexOf(option.id)]?.rowName || ''], - }; - }, - ); - - const selectFieldData: ActivityPhrasalMatrixFieldData = { - type: 'matrix', - values: { byRow, byColumn }, + fieldData = matrixFieldData; + } else if (item.responseType === 'slider') { + (fieldDataContext as ActivityPhrasalDataSliderContext).maxValue = + item.responseValues.maxValue; + + const sliderFieldData: ActivityPhrasalArrayFieldData = { + type: 'array', + values: item.answer.map((value) => `${value ?? ''}`), context: fieldDataContext, }; - fieldData = selectFieldData; + fieldData = sliderFieldData; } else if (item.responseType === 'sliderRows') { (fieldDataContext as ActivityPhrasalDataSliderRowContext).maxValues = item.responseValues.rows.map(({ maxValue }) => maxValue); @@ -175,7 +160,7 @@ export const extractActivitiesPhrasalData = (items: ItemRecord[]): ActivitiesPhr const sliderRowsFieldData: ActivityPhrasalIndexedArrayFieldData = { type: 'indexed-array', values: item.answer.reduce((acc, answerValue, answerIndex) => { - acc[answerIndex] = [`${answerValue || ''}`]; + acc[answerIndex] = [`${answerValue ?? ''}`]; return acc; }, {} as ActivityPhrasalItemizedArrayValue), context: fieldDataContext,