diff --git a/api/converters.py b/api/converters.py index fc8db83b..bc55d134 100644 --- a/api/converters.py +++ b/api/converters.py @@ -232,6 +232,7 @@ def _prep_stage_gate_info( ot_type = STAGE_TYPES_ORIGIN_TRIAL[fe.feature_type] extend_type = STAGE_TYPES_EXTEND_ORIGIN_TRIAL[fe.feature_type] ship_type = STAGE_TYPES_SHIPPING[fe.feature_type] + rollout_type = STAGE_TYPES_ROLLOUT[fe.feature_type] stages = Stage.query(Stage.feature_id == d['id']) major_stages: dict[str, Optional[Stage]] = { @@ -239,7 +240,8 @@ def _prep_stage_gate_info( 'dev_trial': None, 'ot': None, 'extend': None, - 'ship': None} + 'ship': None, + 'rollout': None} # Write a list of stages and gates associated with the feature d['stages'] = stage_helpers.get_feature_stage_ids_list(d['id']) @@ -258,6 +260,8 @@ def _prep_stage_gate_info( major_stages['extend'] = s elif s.stage_type == ship_type: major_stages['ship'] = s + elif s.stage_type == rollout_type: + major_stages['rollout'] = s return major_stages @@ -359,10 +363,10 @@ def feature_entry_to_json_verbose(fe: FeatureEntry) -> dict[str, Any]: # Ship stage fields. d['intent_to_ship_url'] = _stage_attr(stages['ship'], 'intent_thread_url') d['finch_url'] = _stage_attr(stages['ship'], 'finch_url') - d['rollout_milestone'] = _stage_attr(stages['ship'], 'rollout_milestone') - d['rollout_platforms'] = _stage_attr(stages['ship'], 'rollout_platforms') - d['rollout_details'] = _stage_attr(stages['ship'], 'rollout_details') - d['enterprise_policies'] = _stage_attr(stages['ship'], 'enterprise_policies') + d['rollout_milestone'] = _stage_attr(stages['rollout'], 'rollout_milestone') + d['rollout_platforms'] = _stage_attr(stages['rollout'], 'rollout_platforms') + d['rollout_details'] = _stage_attr(stages['rollout'], 'rollout_details') + d['enterprise_policies'] = _stage_attr(stages['rollout'], 'enterprise_policies') impl_status_chrome = d.pop('impl_status_chrome', None) standard_maturity = d.pop('standard_maturity', None) diff --git a/api/processes_api_test.py b/api/processes_api_test.py index 213929aa..a8034704 100644 --- a/api/processes_api_test.py +++ b/api/processes_api_test.py @@ -68,7 +68,6 @@ class ProcessesAPITest(testing_config.CustomTestCase): self.feature_1.feature_type = 0 self.feature_1.breaking_change = True self.feature_1.put() - self.maxDiff = None with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) expected = processes.process_to_dict(processes.BLINK_LAUNCH_PROCESS) @@ -90,7 +89,6 @@ class ProcessesAPITest(testing_config.CustomTestCase): self.feature_1.feature_type = 1 self.feature_1.breaking_change = True self.feature_1.put() - self.maxDiff = None with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) expected = processes.process_to_dict(processes.BLINK_FAST_TRACK_PROCESS) @@ -112,7 +110,6 @@ class ProcessesAPITest(testing_config.CustomTestCase): self.feature_1.feature_type = 2 self.feature_1.breaking_change = True self.feature_1.put() - self.maxDiff = None with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) expected = processes.process_to_dict(processes.PSA_ONLY_PROCESS) @@ -134,7 +131,6 @@ class ProcessesAPITest(testing_config.CustomTestCase): self.feature_1.feature_type = 3 self.feature_1.breaking_change = True self.feature_1.put() - self.maxDiff = None with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) expected = processes.process_to_dict(processes.DEPRECATION_PROCESS) @@ -156,7 +152,6 @@ class ProcessesAPITest(testing_config.CustomTestCase): self.feature_1.feature_type = 4 self.feature_1.breaking_change = True self.feature_1.put() - self.maxDiff = None with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) expected = processes.process_to_dict(processes.ENTERPRISE_PROCESS) @@ -206,7 +201,6 @@ class ProgressAPITest(testing_config.CustomTestCase): with test_app.test_request_context(self.request_path): actual = self.handler.do_get(feature_id=self.feature_id) - self.maxDiff = None self.assertEqual({ 'Code in Chromium': 'True', 'Draft API spec': 'fake spec link', diff --git a/client-src/elements/chromedash-guide-editall-page.js b/client-src/elements/chromedash-guide-editall-page.js index 692ae778..d339deec 100644 --- a/client-src/elements/chromedash-guide-editall-page.js +++ b/client-src/elements/chromedash-guide-editall-page.js @@ -3,7 +3,11 @@ import {ref} from 'lit/directives/ref.js'; import {showToastMessage} from './utils.js'; import './chromedash-form-table'; import './chromedash-form-field'; -import {formatFeatureForEdit, FLAT_FORMS_BY_FEATURE_TYPE} from './form-definition'; +import { + formatFeatureForEdit, + FLAT_FORMS_BY_FEATURE_TYPE, + FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME, + FLAT_ENTERPRISE_PREPARE_TO_SHIP} from './form-definition'; import {SHARED_STYLES} from '../sass/shared-css.js'; import {FORM_STYLES} from '../sass/forms-css.js'; @@ -31,6 +35,7 @@ export class ChromedashGuideEditallPage extends LitElement { super(); this.featureId = 0; this.feature = {}; + this.featureForEdit = {}; this.loading = true; this.appTitle = ''; this.nextPage = ''; @@ -45,6 +50,7 @@ export class ChromedashGuideEditallPage extends LitElement { this.loading = true; window.csClient.getFeature(this.featureId).then((feature) => { this.feature = feature; + this.featureForEdit = formatFeatureForEdit(feature); if (this.feature.name) { document.title = `${this.feature.name} - ${this.appTitle}`; } @@ -85,10 +91,25 @@ export class ChromedashGuideEditallPage extends LitElement { window.location.href = `/guide/edit/${this.featureId}`; } + getForms() { + const forms = JSON.parse(JSON.stringify( + FLAT_FORMS_BY_FEATURE_TYPE[this.featureForEdit.feature_type])); + + // Ensures the rollout field is shown for breaking changes. + if (this.featureForEdit.breaking_change && + !forms.some(([name]) => name === FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME)) { + forms.splice( + forms.length - 1, + 0, + [FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME, FLAT_ENTERPRISE_PREPARE_TO_SHIP]); + } + return forms; + } + // get a comma-spearated list of field names - getFormFields(featureType) { + getFormFields() { let fields = []; - FLAT_FORMS_BY_FEATURE_TYPE[featureType].map((form) => { + this.getForms().map((form) => { fields = [...fields, ...form[1]]; }); return fields.join(); @@ -137,20 +158,19 @@ export class ChromedashGuideEditallPage extends LitElement { } renderForm() { - const formattedFeature = formatFeatureForEdit(this.feature); return html`
- + - ${FLAT_FORMS_BY_FEATURE_TYPE[formattedFeature.feature_type].map(([sectionName, flatFormFields]) => html` + ${this.getForms().map(([sectionName, flatFormFields]) => html`

${sectionName}

${flatFormFields.map((field) => html` + value=${this.featureForEdit[field]}> `)}
diff --git a/client-src/elements/form-definition.js b/client-src/elements/form-definition.js index 12a9d83a..ba73f1c5 100644 --- a/client-src/elements/form-definition.js +++ b/client-src/elements/form-definition.js @@ -203,11 +203,12 @@ const FLAT_SHIP_FIELDS = [ 'shipped_ios_milestone', 'shipped_webview_milestone', ]; -const FLAT_ENTERPRISE_PREPARE_TO_SHIP = [ +export const FLAT_ENTERPRISE_PREPARE_TO_SHIP = [ 'rollout_milestone', 'rollout_platforms', 'rollout_details', 'enterprise_policies', ]; +export const FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME = 'Start feature rollout'; // Forms to be used on the "Edit all" page that shows a flat list of fields. // [[sectionName, flatFormFields]]. @@ -242,7 +243,7 @@ export const FLAT_FORMS_BY_FEATURE_TYPE = { ], [FEATURE_TYPES.FEATURE_TYPE_ENTERPRISE_ID[0]]: [ ['Feature metadata', FLAT_METADATA_FIELDS], - ['Start feature rollout', FLAT_ENTERPRISE_PREPARE_TO_SHIP], + [FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME, FLAT_ENTERPRISE_PREPARE_TO_SHIP], ['Ship', FLAT_SHIP_FIELDS], ], }; diff --git a/client-src/elements/form-field-specs.js b/client-src/elements/form-field-specs.js index 817131c7..42d3f17d 100644 --- a/client-src/elements/form-field-specs.js +++ b/client-src/elements/form-field-specs.js @@ -1142,7 +1142,7 @@ export const ALL_FIELDS = { 'rollout_milestone': { type: 'input', attrs: MILESTONE_NUMBER_FILED_ATTRS, - required: true, + required: false, label: 'Rollout milestone', help_text: html` Milestone in which rollout for this feature starts.`, @@ -1151,7 +1151,7 @@ export const ALL_FIELDS = { 'rollout_platforms': { type: 'multiselect', choices: PLATFORM_CATEGORIES, - required: true, + required: false, label: 'Rollout platforms', help_text: html` Platforms for which rollout for this feature occurs in the selected milestone.`, diff --git a/internals/core_enums.py b/internals/core_enums.py index 5842b148..b844b7dc 100644 --- a/internals/core_enums.py +++ b/internals/core_enums.py @@ -324,6 +324,14 @@ STAGE_TYPES_SHIPPING: dict[int, Optional[int]] = { FEATURE_TYPE_EXISTING_ID: STAGE_FAST_SHIPPING, FEATURE_TYPE_CODE_CHANGE_ID: STAGE_PSA_SHIPPING, FEATURE_TYPE_DEPRECATION_ID: STAGE_DEP_SHIPPING, + FEATURE_TYPE_ENTERPRISE_ID: None + } +# Rollout stage types for every feature type. +STAGE_TYPES_ROLLOUT: dict[int, Optional[int]] = { + FEATURE_TYPE_INCUBATE_ID: STAGE_ENT_ROLLOUT, + FEATURE_TYPE_EXISTING_ID: STAGE_ENT_ROLLOUT, + FEATURE_TYPE_CODE_CHANGE_ID: STAGE_ENT_ROLLOUT, + FEATURE_TYPE_DEPRECATION_ID: STAGE_ENT_ROLLOUT, FEATURE_TYPE_ENTERPRISE_ID: STAGE_ENT_ROLLOUT } @@ -364,10 +372,10 @@ STAGE_TYPES_BY_FIELD_MAPPING: dict[str, dict[int, Optional[int]]] = { 'dt_milestone_android_start': STAGE_TYPES_DEV_TRIAL, 'dt_milestone_ios_start': STAGE_TYPES_DEV_TRIAL, 'dt_milestone_webview_start': STAGE_TYPES_DEV_TRIAL, - 'enterprise_policies': STAGE_TYPES_SHIPPING, - 'rollout_milestone': STAGE_TYPES_SHIPPING, - 'rollout_platforms': STAGE_TYPES_SHIPPING, - 'rollout_details': STAGE_TYPES_SHIPPING + 'enterprise_policies': STAGE_TYPES_ROLLOUT, + 'rollout_milestone': STAGE_TYPES_ROLLOUT, + 'rollout_platforms': STAGE_TYPES_ROLLOUT, + 'rollout_details': STAGE_TYPES_ROLLOUT } # Mapping of which stage types are associated with each gate type. diff --git a/internals/processes.py b/internals/processes.py index 94b674a4..2002afec 100644 --- a/internals/processes.py +++ b/internals/processes.py @@ -17,6 +17,8 @@ from dataclasses import asdict, dataclass from internals import approval_defs from internals import core_enums +from internals import core_models +from internals import stage_helpers @dataclass @@ -702,18 +704,18 @@ PROGRESS_DETECTORS = { lambda f, _: f.impl_status_chrome == core_enums.REMOVED, 'Rollout milestone': - lambda f, stages: stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]] and - stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]][0].rollout_milestone, + lambda f, stages: stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]] and + stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]][0].rollout_milestone, 'Rollout platforms': - lambda f, stages: stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]] and - stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]][0].rollout_platforms, + lambda f, stages: stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]] and + stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]][0].rollout_platforms, 'Rollout details': - lambda f, stages: stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]] and - stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]][0].rollout_details, + lambda f, stages: stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]] and + stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]][0].rollout_details, 'Enterprise policies': - lambda f, stages: stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]] and - stages[core_enums.STAGE_TYPES_SHIPPING[f.feature_type]][0].enterprise_policies, + lambda f, stages: stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]] and + stages[core_enums.STAGE_TYPES_ROLLOUT[f.feature_type]][0].enterprise_policies, } diff --git a/internals/processes_test.py b/internals/processes_test.py index b01ef3cd..bdde8fa3 100644 --- a/internals/processes_test.py +++ b/internals/processes_test.py @@ -154,7 +154,7 @@ class ProgressDetectorsTest(testing_config.CustomTestCase): name='feature one', summary='sum', category=1, intent_stage=core_enums.INTENT_IMPLEMENT, feature_type=0) self.feature_1.put() - stage_types = [110, 120, 130, 140, 150, 151, 160] + stage_types = [110, 120, 130, 140, 150, 151, 160, 1061] self.stages: list[core_models.Stage] = [] for s_type in stage_types: stage = core_models.Stage(feature_id=self.feature_1.key.integer_id(), @@ -281,23 +281,23 @@ class ProgressDetectorsTest(testing_config.CustomTestCase): def test_rollout_milestone(self): detector = processes.PROGRESS_DETECTORS['Rollout milestone'] self.assertFalse(detector(self.feature_1, self.stages_dict)) - self.stages_dict[160][0].rollout_milestone = 99 + self.stages_dict[1061][0].rollout_milestone = 99 self.assertTrue(detector(self.feature_1, self.stages_dict)) def test_rollout_platforms(self): detector = processes.PROGRESS_DETECTORS['Rollout platforms'] self.assertFalse(detector(self.feature_1, self.stages_dict)) - self.stages_dict[160][0].rollout_platforms = ['iOS', 'Android'] + self.stages_dict[1061][0].rollout_platforms = ['iOS', 'Android'] self.assertTrue(detector(self.feature_1, self.stages_dict)) def test_rollout_details(self): detector = processes.PROGRESS_DETECTORS['Rollout details'] self.assertFalse(detector(self.feature_1, self.stages_dict)) - self.stages_dict[160][0].rollout_details = 'Details' + self.stages_dict[1061][0].rollout_details = 'Details' self.assertTrue(detector(self.feature_1, self.stages_dict)) def test_enterprise_policies(self): detector = processes.PROGRESS_DETECTORS['Enterprise policies'] self.assertFalse(detector(self.feature_1, self.stages_dict)) - self.stages_dict[160][0].enterprise_policies = ['Policy1', 'Policy2'] + self.stages_dict[1061][0].enterprise_policies = ['Policy1', 'Policy2'] self.assertTrue(detector(self.feature_1, self.stages_dict))