From 3db1ac5cc8685dfb23d6c215e983287aff0a0242 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Fri, 3 Jul 2026 15:13:00 +1000 Subject: [PATCH 1/2] Support for upload type for PM-5516 --- .../SubmissionPage/Submit/index.jsx | 62 ++++++++++++++ .../FilestackFilePicker/index.jsx | 3 +- .../SubmissionPage/Submit/index.jsx | 84 ++++++++++++++----- .../components/SubmissionPage/index.jsx | 1 + src/shared/containers/SubmissionPage.jsx | 2 + 5 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 __tests__/shared/components/SubmissionPage/Submit/index.jsx diff --git a/__tests__/shared/components/SubmissionPage/Submit/index.jsx b/__tests__/shared/components/SubmissionPage/Submit/index.jsx new file mode 100644 index 000000000..57ad49ff8 --- /dev/null +++ b/__tests__/shared/components/SubmissionPage/Submit/index.jsx @@ -0,0 +1,62 @@ +import { resolveSubmissionMode } from 'components/SubmissionPage/Submit'; + +const topgearCommunitiesList = { + data: [ + { + groupIds: ['wipro-group-id'], + mainSubdomain: 'topgear', + }, + ], + loadingUuid: '', + timestamp: Date.now(), +}; + +describe('resolveSubmissionMode', () => { + test('uses zip upload when submission_type metadata is zip', () => { + expect(resolveSubmissionMode( + [{ name: 'submission_type', value: 'zip' }], + ['wipro-group-id'], + topgearCommunitiesList, + )).toEqual({ + isLoadingCommunitiesList: false, + isUrlSubmission: false, + }); + }); + + test('uses URL upload when submission_type metadata is url', () => { + expect(resolveSubmissionMode( + [{ name: 'submission_type', value: 'url' }], + [], + topgearCommunitiesList, + )).toEqual({ + isLoadingCommunitiesList: false, + isUrlSubmission: true, + }); + }); + + test('falls back to URL upload for Topgear groups when metadata is absent', () => { + expect(resolveSubmissionMode( + [], + ['wipro-group-id'], + topgearCommunitiesList, + )).toEqual({ + isLoadingCommunitiesList: false, + isUrlSubmission: true, + }); + }); + + test('waits for communities list before applying the legacy group fallback', () => { + expect(resolveSubmissionMode( + [], + ['wipro-group-id'], + { + data: [], + loadingUuid: '', + timestamp: 0, + }, + )).toEqual({ + isLoadingCommunitiesList: true, + isUrlSubmission: false, + }); + }); +}); diff --git a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx index b9b2dd8d0..7a39b8ea2 100644 --- a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx +++ b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx @@ -4,7 +4,8 @@ * * Description: * Component for uploading a file using Filestack Picker - * and Drag + Drop. Does not store the file contents in form. Instead, + * and Drag + Drop, or accepting a URL for Topgear submissions. + * Does not store the file contents in form. Instead, * uploads file to S3 storage container and sets the * S3 storage details to Redux store for submission. */ diff --git a/src/shared/components/SubmissionPage/Submit/index.jsx b/src/shared/components/SubmissionPage/Submit/index.jsx index 662fef81e..ca883ae00 100644 --- a/src/shared/components/SubmissionPage/Submit/index.jsx +++ b/src/shared/components/SubmissionPage/Submit/index.jsx @@ -4,7 +4,8 @@ * * Description: * Page that is shown when a user is trying to submit a Submission. - * Allows user to upload Submission.zip file using a Filestack plugin. + * Allows users to submit either a standard zip upload or a Topgear URL, + * depending on challenge metadata and legacy Topgear group defaults. */ /* eslint-env browser */ @@ -21,6 +22,58 @@ import FilestackFilePicker from '../FilestackFilePicker'; import Uploading from '../Uploading'; import style from './styles.scss'; +const SUBMISSION_TYPE_METADATA_FIELD = 'submission_type'; +const SUBMISSION_TYPE_URL = 'url'; +const SUBMISSION_TYPE_ZIP = 'zip'; + +/** + * Resolves which submission experience should be shown for the challenge. + * Explicit `submission_type` metadata wins over the historical Topgear group + * check. When the metadata is absent or invalid, the current fallback remains: + * Topgear/Wipro challenges use URL submission and all others use zip upload. + * + * @param {Array} metadata Challenge metadata entries. + * @param {Array} groups Challenge group ids. + * @param {Object} communitiesList Loaded community metadata from Redux. + * @return {{isLoadingCommunitiesList: Boolean, isUrlSubmission: Boolean}} + */ +export function resolveSubmissionMode(metadata, groups, communitiesList) { + const submissionType = _.toLower(_.toString(_.get( + _.find(metadata, { name: SUBMISSION_TYPE_METADATA_FIELD }), + 'value', + '', + )).trim()); + + if (submissionType === SUBMISSION_TYPE_ZIP || submissionType === SUBMISSION_TYPE_URL) { + return { + isLoadingCommunitiesList: false, + isUrlSubmission: submissionType === SUBMISSION_TYPE_URL, + }; + } + + if (_.isEmpty(groups)) { + return { + isLoadingCommunitiesList: false, + isUrlSubmission: false, + }; + } + + if (!communitiesList.timestamp) { + return { + isLoadingCommunitiesList: true, + isUrlSubmission: false, + }; + } + + const topGearCommunity = _.find(communitiesList.data, { mainSubdomain: 'topgear' }); + const topGearGroupIds = _.get(topGearCommunity, 'groupIds', []); + + return { + isLoadingCommunitiesList: false, + isUrlSubmission: _.some(groups, groupId => groupId && _.includes(topGearGroupIds, groupId)), + }; +} + /** * Submissions Page shown to develop challengers. */ @@ -137,31 +190,15 @@ class Submit extends React.Component { setSubmissionFilestackData, submitForm, groups, + metadata, } = this.props; const id = 'file-picker-submission'; - - let isLoadingCommunitiesList = false; - let isChallengeBelongToTopgearGroup = false; - // check if challenge belong to any group - if (!_.isEmpty(groups)) { - // check if communitiesList is loaded - if (communitiesList.timestamp > 0) { - const topGearCommunity = _.find(communitiesList.data, { mainSubdomain: 'topgear' }); - if (topGearCommunity) { - // check the group info match with group list - _.forOwn(groups, (value) => { - if (value && _.includes(topGearCommunity.groupIds, value)) { - isChallengeBelongToTopgearGroup = true; - return false; - } - return true; - }); - } - } else { - isLoadingCommunitiesList = true; - } - } + const submissionMode = resolveSubmissionMode(metadata, groups, communitiesList); + const { + isLoadingCommunitiesList, + isUrlSubmission: isChallengeBelongToTopgearGroup, + } = submissionMode; const submissionInstruction = isChallengeBelongToTopgearGroup ? (
@@ -427,6 +464,7 @@ Submit.propTypes = { timestamp: PT.number.isRequired, }).isRequired, groups: PT.arrayOf(PT.shape()).isRequired, + metadata: PT.arrayOf(PT.shape()).isRequired, isSubmitting: PT.bool.isRequired, submitDone: PT.bool.isRequired, errorMsg: PT.string, diff --git a/src/shared/components/SubmissionPage/index.jsx b/src/shared/components/SubmissionPage/index.jsx index bd4dded61..72b3e8a8a 100644 --- a/src/shared/components/SubmissionPage/index.jsx +++ b/src/shared/components/SubmissionPage/index.jsx @@ -87,6 +87,7 @@ SubmissionsPage.propTypes = { timestamp: PT.number.isRequired, }).isRequired, groups: PT.arrayOf(PT.shape()).isRequired, + metadata: PT.arrayOf(PT.shape()).isRequired, track: PT.string.isRequired, status: PT.string.isRequired, submitForm: PT.func.isRequired, diff --git a/src/shared/containers/SubmissionPage.jsx b/src/shared/containers/SubmissionPage.jsx index 88be1deed..4a2971b96 100644 --- a/src/shared/containers/SubmissionPage.jsx +++ b/src/shared/containers/SubmissionPage.jsx @@ -139,6 +139,7 @@ SubmissionsPageContainer.propTypes = { status: PT.string.isRequired, isRegistered: PT.bool.isRequired, groups: PT.arrayOf(PT.shape()).isRequired, + metadata: PT.arrayOf(PT.shape()).isRequired, errorMsg: PT.string.isRequired, isSubmitting: PT.bool.isRequired, submitDone: PT.bool.isRequired, @@ -193,6 +194,7 @@ const mapStateToProps = (state, ownProps) => { status: details.status, isRegistered: details.isRegistered, groups: details.groups, + metadata: details.metadata || [], isSubmitting: submission.isSubmitting, submitDone: submission.submitDone, errorMsg: submission.submitErrorMsg, From 553185c779eff3991a293d8725463cc75864ec30 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Fri, 3 Jul 2026 15:33:34 +1000 Subject: [PATCH 2/2] NDA fixes for PM-5330 --- __tests__/shared/utils/terms.test.js | 8 +++++++- config/backup-default.js | 6 +++--- config/default.js | 6 +++--- src/shared/utils/terms.js | 10 +++++++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/__tests__/shared/utils/terms.test.js b/__tests__/shared/utils/terms.test.js index ca7167f14..4d262712b 100644 --- a/__tests__/shared/utils/terms.test.js +++ b/__tests__/shared/utils/terms.test.js @@ -12,10 +12,16 @@ describe('terms utils', () => { expect(isNdaTerm({ title: 'Assignment Terms' })).toBe(false); }); - test('uses configured DocuSign template for NDA terms', () => { + test('uses terms-service DocuSign template for NDA terms when present', () => { expect(getDocuSignTemplateIdForTerm({ docusignTemplateId: 'old-template-id', title: 'Appirio NDA v2.0', + })).toBe('old-template-id'); + }); + + test('uses configured DocuSign template for NDA terms without a template id', () => { + expect(getDocuSignTemplateIdForTerm({ + title: 'Appirio NDA v2.0', })).toBe(NEW_NDA_TEMPLATE_ID); }); diff --git a/config/backup-default.js b/config/backup-default.js index 40c716e38..2b0d74756 100644 --- a/config/backup-default.js +++ b/config/backup-default.js @@ -70,9 +70,9 @@ module.exports = { * agreement flow. */ MOCK_TERMS_SERVICE: false, - /* Optional DocuSign template override for NDA-like terms. When set, the - * terms modal requests this template for terms whose title includes NDA or - * Non-Disclosure, even if the Terms API still returns an older template id. + /* Optional DocuSign template fallback for NDA-like terms. The terms modal + * prefers the template id returned by the Terms API because backend + * completion is persisted against that same template id. */ NDA_DOCUSIGN_TEMPLATE_ID: '400b989d-1c75-4889-b6f6-421e1f924709', diff --git a/config/default.js b/config/default.js index c8f23a3e4..ec826e361 100644 --- a/config/default.js +++ b/config/default.js @@ -72,9 +72,9 @@ module.exports = { * agreement flow. */ MOCK_TERMS_SERVICE: false, - /* Optional DocuSign template override for NDA-like terms. When set, the - * terms modal requests this template for terms whose title includes NDA or - * Non-Disclosure, even if the Terms API still returns an older template id. + /* Optional DocuSign template fallback for NDA-like terms. The terms modal + * prefers the template id returned by the Terms API because backend + * completion is persisted against that same template id. */ NDA_DOCUSIGN_TEMPLATE_ID: '400b989d-1c75-4889-b6f6-421e1f924709', diff --git a/src/shared/utils/terms.js b/src/shared/utils/terms.js index 192e6d6ff..c3b4df545 100644 --- a/src/shared/utils/terms.js +++ b/src/shared/utils/terms.js @@ -16,14 +16,18 @@ export function isNdaTerm(term = {}) { * Resolves the DocuSign template id to use for a terms-service record. * * @param {Object|null} term terms-service record or details payload. - * @returns {String|Number|undefined} configured NDA template id for NDA terms, - * or the template id returned by terms-service for all other terms. + * @returns {String|Number|undefined} the template id returned by terms-service, + * or the configured NDA fallback when an NDA term has no template id. */ export function getDocuSignTemplateIdForTerm(term = {}) { + if (term && term.docusignTemplateId) { + return term.docusignTemplateId; + } + const configuredNdaTemplateId = config.NDA_DOCUSIGN_TEMPLATE_ID; if (configuredNdaTemplateId && isNdaTerm(term)) { return configuredNdaTemplateId; } - return term ? term.docusignTemplateId : undefined; + return undefined; }