diff --git a/client-src/css/forms-css.js b/client-src/css/forms-css.js index 2069fd9aaf92..956ace7bebc4 100644 --- a/client-src/css/forms-css.js +++ b/client-src/css/forms-css.js @@ -78,6 +78,19 @@ export const FORM_STYLES = [ margin-top: 2em; } + .fade-in { + animation:fadeIn 0.5s linear; + } + + @keyframes fadeIn { + 0% { + opacity:0 + } + 100% { + opacity:1; + } + } + #metadata, .stage_form, .flat_form, .final_buttons { max-width: 67em; padding: 1em; diff --git a/client-src/elements/chromedash-form-field.js b/client-src/elements/chromedash-form-field.js index c41898c92374..8e66093b5f80 100644 --- a/client-src/elements/chromedash-form-field.js +++ b/client-src/elements/chromedash-form-field.js @@ -13,6 +13,7 @@ export class ChromedashFormField extends LitElement { index: {type: Number}, // Represents which field this is on the form. disabled: {type: Boolean}, checkboxLabel: {type: String}, // Optional override of default label. + shouldFadeIn: {type: Boolean}, loading: {type: Boolean}, fieldProps: {type: Object}, forEnterprise: {type: Boolean}, @@ -28,6 +29,7 @@ export class ChromedashFormField extends LitElement { this.index = -1; this.checkboxLabel = ''; this.disabled = false; + this.shouldFadeIn = false; this.loading = false; this.forEnterprise = false; this.stageType = undefined; @@ -234,15 +236,16 @@ export class ChromedashFormField extends LitElement { this.forEnterprise && (this.fieldProps.enterprise_extra_help !== undefined) ? this.fieldProps.enterprise_extra_help : this.fieldProps.extra_help; + const fadeInClass = (this.shouldFadeIn) ? 'fade-in' : ''; return html` ${this.fieldProps.label ? html` - + ${this.fieldProps.label}: `:nothing} - + ${this.renderWidgets()} ${helpText ? html` ${helpText} `: nothing} diff --git a/client-src/elements/chromedash-ot-creation-page.js b/client-src/elements/chromedash-ot-creation-page.js index 0a74c2741a33..a540b0d2934e 100644 --- a/client-src/elements/chromedash-ot-creation-page.js +++ b/client-src/elements/chromedash-ot-creation-page.js @@ -1,4 +1,4 @@ -import {LitElement, css, html} from 'lit'; +import {LitElement, css, html, nothing} from 'lit'; import {ref} from 'lit/directives/ref.js'; import { formatFeatureChanges, @@ -10,6 +10,7 @@ import './chromedash-form-field.js'; import {ORIGIN_TRIAL_CREATION_FIELDS} from './form-definition.js'; import {SHARED_STYLES} from '../css/shared-css.js'; import {FORM_STYLES} from '../css/forms-css.js'; +import {ALL_FIELDS} from './form-field-specs.js'; export class ChromedashOTCreationPage extends LitElement { @@ -31,6 +32,7 @@ export class ChromedashOTCreationPage extends LitElement { appTitle: {type: String}, nextPage: {type: String}, fieldValues: {type: Array}, + showApprovalsFields: {type: Boolean}, }; } @@ -43,6 +45,7 @@ export class ChromedashOTCreationPage extends LitElement { this.appTitle = ''; this.nextPage = ''; this.fieldValues = []; + this.showApprovalsFields = false; } connectedCallback() { @@ -61,6 +64,11 @@ export class ChromedashOTCreationPage extends LitElement { // The field has been updated, so it is considered touched. this.fieldValues[index].touched = true; this.fieldValues[index].value = value; + + if (this.fieldValues[index].name === 'ot_require_approvals') { + this.showApprovalsFields = !this.showApprovalsFields; + this.requestUpdate(); + } }; fetchData() { @@ -75,12 +83,78 @@ export class ChromedashOTCreationPage extends LitElement { if (this.feature.name) { document.title = `${this.feature.name} - ${this.appTitle}`; } + this.setFieldValues(); this.loading = false; - }).catch(() => { - showToastMessage('Some errors occurred. Please refresh the page or try again later.'); }); } + addOptionalApprovalsFields() { + // Approval requirement fields are always hidden unless the feature owner + // opts in to using them. + const insertIndex = this.fieldValues.findIndex( + fieldInfo => fieldInfo.name === 'ot_require_approvals') + 1; + this.fieldValues.splice(insertIndex, 0, + { + name: 'ot_approval_buganizer_component', + touched: true, + value: '', + stageId: this.stage.id, + isApprovalsField: true, + }, + { + name: 'ot_approval_group_email', + touched: true, + value: '', + stageId: this.stage.id, + isApprovalsField: true, + }, + { + name: 'ot_approval_criteria_url', + touched: true, + value: '', + stageId: this.stage.id, + isApprovalsField: true, + }); + } + + addHiddenFields() { + // Add a field for updating that an OT creation request has been submitted. + this.fieldValues.push({ + name: 'ot_action_requested', + touched: true, + value: true, + stageId: this.stage.id, + alwaysHidden: true, + }); + } + + setFieldValues() { + // OT creation page only has one section. + const section = ORIGIN_TRIAL_CREATION_FIELDS.sections[0]; + + this.fieldValues = section.fields.map(field => { + const featureJSONKey = ALL_FIELDS[field].name || field; + let value = getStageValue(this.stage, featureJSONKey); + let touched = false; + + // The requester's email should be a contact by default. + if (featureJSONKey === 'ot_owner_email' && !value) { + value = [this.userEmail]; + touched = true; + } + + // Add the field to this component's stage before creating the field component. + return { + name: featureJSONKey, + touched, + value, + stageId: this.stage.id, + }; + }); + this.addOptionalApprovalsFields(); + this.addHiddenFields(); + } + disconnectedCallback() { super.disconnectedCallback(); document.title = this.appTitle; @@ -111,8 +185,6 @@ export class ChromedashOTCreationPage extends LitElement { setTimeout(() => { window.location.href = this.nextPage || `/feature/${this.featureId}`; }, 1000); - }).catch(() => { - showToastMessage('Some errors occurred. Please refresh the page or try again later.'); }); } @@ -156,41 +228,24 @@ export class ChromedashOTCreationPage extends LitElement { `; } - renderFields(section) { - const fields = section.fields.map(field => { - let value = getStageValue(this.stage, field); - let touched = false; - // The requester's email should be a contact by default. - if (field === 'ot_owner_email' && !value) { - value = [this.userEmail]; - touched = true; + renderFields() { + const fields = this.fieldValues.map((fieldInfo, i) => { + if (fieldInfo.alwaysHidden || (fieldInfo.isApprovalsField && !this.showApprovalsFields)) { + return nothing; } - // Add the field to this component's stage before creating the field component. - const index = this.fieldValues.length; - this.fieldValues.push({ - name: field, - touched, - value, - stageId: this.stage.id, - }); + // Fade in transition for the approvals fields if they're being displayed. + const shouldFadeIn = fieldInfo.isApprovalsField; return html` `; }); - - // Add a field for updating that an OT creation request has been submitted. - this.fieldValues.push({ - name: 'ot_action_requested', - touched: true, - value: true, - stageId: this.stage.id, - }); return fields; } diff --git a/client-src/elements/form-definition.js b/client-src/elements/form-definition.js index b53d3a160a52..af210de059a9 100644 --- a/client-src/elements/form-definition.js +++ b/client-src/elements/form-definition.js @@ -527,6 +527,7 @@ export const ORIGIN_TRIAL_CREATION_FIELDS = { 'ot_is_deprecation_trial', 'ot_has_third_party_support', 'ot_is_critical_trial', + 'ot_require_approvals', 'ot_request_note', ], }, diff --git a/client-src/elements/form-field-enums.js b/client-src/elements/form-field-enums.js index 50e66367e605..19f4a3e37cb2 100644 --- a/client-src/elements/form-field-enums.js +++ b/client-src/elements/form-field-enums.js @@ -261,6 +261,10 @@ export const STAGE_SPECIFIC_FIELDS = new Set([ 'ot_creation__milestone_desktop_first', 'ot_creation__milestone_desktop_last', 'ot_extension__milestone_desktop_last', + 'ot_require_approvals', + 'ot_approval_buganizer_component', + 'ot_approval_group_email', + 'ot_approval_criteria_url', 'finch_url', 'experiment_goals', 'experiment_risks', diff --git a/client-src/elements/form-field-specs.js b/client-src/elements/form-field-specs.js index 3a3bf735d6d7..2f7068976de6 100644 --- a/client-src/elements/form-field-specs.js +++ b/client-src/elements/form-field-specs.js @@ -1128,6 +1128,46 @@ export const ALL_FIELDS = { >web_feature.mojom.`, }, + 'ot_require_approvals': { + type: 'checkbox', + initial: false, + label: 'Trial participation requires approval', + help_text: html` + Will this trial require registrants to receive approval before + participating? See information about this setting at + Some link`, + }, + + 'ot_approval_buganizer_component': { + type: 'input', + attrs: {type: 'number'}, + required: true, + label: 'Approvals Buganizer component ID', + help_text: html` + Buganizer component ID used for approvals requests.`, + }, + + 'ot_approval_group_email': { + type: 'input', + required: true, + attrs: {...TEXT_FIELD_ATTRS, pattern: GOOGLE_EMAIL_ADDRESS_REGEX}, + label: 'Registration request notifications group', + help_text: html` +

+ Google group email to be used for new registration request notifications. + Please supply a '@google.com' domain email address only. +

`, + }, + + 'ot_approval_criteria_url': { + type: 'input', + attrs: URL_FIELD_ATTRS, + required: true, + label: 'Approval criteria link', + help_text: html` + Link to documentation describing the requirements to be approved for trial participation.`, + }, + 'ot_creation__intent_to_experiment_url': { name: 'intent_to_experiment_url', type: 'input',