Show and edit the active stage in the metadata section. (#4047)

This commit is contained in:
Jeffrey Yasskin 2024-07-03 09:55:26 -07:00 коммит произвёл GitHub
Родитель 401ff31f14
Коммит 55e6a2b8c0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 199 добавлений и 36 удалений

Просмотреть файл

@ -1,8 +1,8 @@
import {LitElement, html, nothing} from 'lit';
import {ALL_FIELDS} from './form-field-specs';
import {ref} from 'lit/directives/ref.js';
import './chromedash-textarea';
import {showToastMessage, getFieldValueFromFeature} from './utils.js';
import {ALL_FIELDS, resolveFieldForFeature} from './form-field-specs';
import {getFieldValueFromFeature, showToastMessage} from './utils.js';
export class ChromedashFormField extends LitElement {
static get properties() {
@ -12,6 +12,7 @@ export class ChromedashFormField extends LitElement {
stageId: {type: Number},
value: {type: String},
fieldValues: {type: Array}, // All other field value objects in current form.
feature: {attribute: false}, // The rest of the feature being edited.
disabled: {type: Boolean},
checkboxLabel: {type: String}, // Optional override of default label.
shouldFadeIn: {type: Boolean},
@ -31,6 +32,8 @@ export class ChromedashFormField extends LitElement {
this.value = '';
this.initialValue = '';
this.fieldValues = [];
/** @type {import('./form-definition').FormattedFeature} */
this.feature = {};
this.checkboxLabel = '';
this.disabled = false;
this.shouldFadeIn = false;
@ -56,7 +59,10 @@ export class ChromedashFormField extends LitElement {
connectedCallback() {
super.connectedCallback();
this.fieldProps = ALL_FIELDS[this.name] || {};
this.fieldProps = resolveFieldForFeature(
ALL_FIELDS[this.name] || {},
this.feature
);
// Register this form field component with the page component.
const app = document.querySelector('chromedash-app');

Просмотреть файл

@ -1,6 +1,12 @@
import {html} from 'lit';
import {assert, fixture} from '@open-wc/testing';
import '@shoelace-style/shoelace/dist/components/option/option.js';
import {html} from 'lit';
import {ChromedashFormField} from './chromedash-form-field';
import {
STAGE_BLINK_INCUBATE,
STAGE_BLINK_ORIGIN_TRIAL,
STAGE_BLINK_SHIPPING,
} from './form-field-enums';
describe('chromedash-form-field', () => {
it('renders a checkbox type of field', async () => {
@ -107,4 +113,42 @@ describe('chromedash-form-field', () => {
assert.include(renderElement.innerHTML, 'multiple');
assert.include(renderElement.innerHTML, 'cleareable');
});
describe('complex fields', async () => {
it('active_stage_id depends on the available stages', async () => {
/** @type {import('./form-definition').FormattedFeature} */
const formattedFeature = {
stages: [
{id: 1, stage_type: STAGE_BLINK_INCUBATE},
{
id: 2,
stage_type: STAGE_BLINK_ORIGIN_TRIAL,
display_name: 'Display name',
},
{
id: 3,
stage_type: STAGE_BLINK_ORIGIN_TRIAL,
},
{id: 4, stage_type: STAGE_BLINK_SHIPPING},
{id: 5, stage_type: 9999, display_name: 'Not a stage type'},
],
};
const component = await fixture(html`
<chromedash-form-field
name="active_stage_id"
.feature=${formattedFeature}
></chromedash-form-field>
`);
assert.instanceOf(component, ChromedashFormField);
const optionValues = Array.from(
component.renderRoot.querySelectorAll('sl-option')
).map(option => ({text: option.textContent.trim(), value: option.value}));
assert.deepEqual(optionValues, [
{text: 'Identify the need', value: '1'},
{text: 'Origin trial: Display name', value: '2'},
{text: 'Origin trial 2', value: '3'},
{text: 'Prepare to ship', value: '4'},
]);
});
});
});

Просмотреть файл

@ -283,6 +283,7 @@ export class ChromedashGuideEditallPage extends LitElement {
stageId=${stageId}
value=${value}
.fieldValues=${this.fieldValues}
.feature=${formattedFeature}
?forEnterprise=${formattedFeature.is_enterprise_feature}
@form-field-update="${this.handleFormFieldUpdate}"
>

Просмотреть файл

@ -180,6 +180,7 @@ export class ChromedashGuideMetadataPage extends LitElement {
index=${index}
value=${value}
.fieldValues=${this.fieldValues}
.feature=${formattedFeature}
?forEnterprise=${formattedFeature.is_enterprise_feature}
@form-field-update="${this.handleFormFieldUpdate}"
>

Просмотреть файл

@ -246,6 +246,7 @@ export class ChromedashGuideStagePage extends LitElement {
index=${index}
value=${value}
.fieldValues=${this.fieldValues}
.feature=${formattedFeature}
stageId=${feStage.id}
?forEnterprise=${formattedFeature.is_enterprise_feature}
@form-field-update="${this.handleFormFieldUpdate}"
@ -276,6 +277,7 @@ export class ChromedashGuideStagePage extends LitElement {
index=${index}
value=${this.isActiveStage}
.fieldValues=${this.fieldValues}
.feature=${formattedFeature}
?disabled=${this.isActiveStage}
?forEnterprise=${formattedFeature.is_enterprise_feature}
@form-field-update="${this.handleFormFieldUpdate}"

Просмотреть файл

@ -212,6 +212,7 @@ export class ChromedashGuideVerifyAccuracyPage extends LitElement {
index=${index}
value=${value}
.fieldValues=${this.fieldValues}
.feature=${formattedFeature}
?forEnterprise=${formattedFeature.is_enterprise_feature}
@form-field-update="${this.handleFormFieldUpdate}"
>

Просмотреть файл

@ -1,8 +1,8 @@
import {html, TemplateResult} from 'lit';
import {Feature} from '../js-src/cs-client';
import {Feature, StageDict} from '../js-src/cs-client';
import * as enums from './form-field-enums';
interface FormattedFeature {
export interface FormattedFeature {
category: number;
enterprise_feature_categories: string[];
feature_type: number;
@ -36,6 +36,7 @@ interface FormattedFeature {
web_dev_views_link?: string;
web_dev_views_notes?: string;
other_views_notes?: string;
stages: StageDict[];
[key: string]: any; // Allow additional properties
}
@ -198,6 +199,7 @@ export const FLAT_METADATA_FIELDS: MetadataFields = {
'devrel',
'category',
'feature_type',
'active_stage_id',
'search_tags',
],
},
@ -486,10 +488,10 @@ const PSA_PREPARE_TO_SHIP_FIELDS: MetadataFields = {
};
const DEPRECATION_PLAN_FIELDS: MetadataFields = {
name: 'Write up motivation',
name: 'Write up deprecation plan',
sections: [
{
name: 'Write up motivation',
name: 'Write up deprecation plan',
fields: ['motivation', 'spec_link'],
},
],
@ -587,10 +589,10 @@ export const ORIGIN_TRIAL_EXTENSION_FIELDS: MetadataFields = {
// Note: Even though this is similar to another form, it is likely to change.
const DEPRECATION_ORIGIN_TRIAL_FIELDS: MetadataFields = {
name: 'Origin trial',
name: 'Prepare for Deprecation Trial',
sections: [
{
name: 'Origin trial',
name: 'Prepare for Deprecation Trial',
fields: [
'display_name',
'experiment_goals',

Просмотреть файл

@ -1,26 +1,27 @@
import {html, TemplateResult} from 'lit';
import {FormattedFeature} from './form-definition.js';
import {
DT_MILESTONE_FIELDS,
ENTERPRISE_FEATURE_CATEGORIES,
ENTERPRISE_IMPACT,
FEATURE_CATEGORIES,
FEATURE_TYPES,
FEATURE_TYPES_WITHOUT_ENTERPRISE,
IMPLEMENTATION_STATUS,
PLATFORM_CATEGORIES,
ROLLOUT_IMPACT,
STANDARD_MATURITY_CHOICES,
REVIEW_STATUS_CHOICES,
VENDOR_VIEWS_COMMON,
VENDOR_VIEWS_GECKO,
WEB_DEV_VIEWS,
DT_MILESTONE_FIELDS,
OT_MILESTONE_START_FIELDS,
PLATFORM_CATEGORIES,
REVIEW_STATUS_CHOICES,
ROLLOUT_IMPACT,
SHIPPED_MILESTONE_FIELDS,
STAGE_TYPES_DEV_TRIAL,
STAGE_TYPES_ORIGIN_TRIAL,
STAGE_TYPES_SHIPPING,
ENTERPRISE_IMPACT,
STANDARD_MATURITY_CHOICES,
VENDOR_VIEWS_COMMON,
VENDOR_VIEWS_GECKO,
WEB_DEV_VIEWS,
} from './form-field-enums';
import {error} from 'console';
import {unambiguousStageName} from './utils';
interface FieldAttrs {
title?: string;
@ -46,11 +47,9 @@ interface MilestoneRange {
error?: string;
}
type FeatureName = string;
interface Field {
interface ResolvedField {
type?: string;
name?: FeatureName;
name?: keyof FormattedFeature;
attrs?: FieldAttrs;
required?: boolean;
label?: string;
@ -68,6 +67,25 @@ interface Field {
disabled?: boolean;
}
interface Field extends ResolvedField {
computedChoices?: (
feature: FormattedFeature
) => Record<string, [number, string]>;
}
/** Computes values for any field property that depends on other parts of the feature's state.
*/
export function resolveFieldForFeature(
field: Field,
feature: FormattedFeature
): ResolvedField {
const result: ResolvedField = {...field};
if (field.computedChoices) {
result.choices = field.computedChoices(feature);
}
return result;
}
/* Patterns from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s01.html
* Removing single quote ('), backtick (`), and pipe (|) since they are risky unless properly escaped everywhere.
* Also removing ! and % because they have special meaning for some older email routing systems. */
@ -476,6 +494,24 @@ export const ALL_FIELDS: Record<string, Field> = {
check: (_value, getFieldValue) => checkFeatureNameAndType(getFieldValue),
},
active_stage_id: {
type: 'select',
computedChoices(formattedFeature) {
const result: Record<string, [number, string]> = {};
for (const stage of formattedFeature.stages) {
const name = unambiguousStageName(stage, formattedFeature);
if (name) {
result[stage.id] = [stage.id, name];
}
}
return result;
},
label: 'Active stage',
help_text: html`The active stage sets which stage opens by default in this
feature's page. This is equivalent to editing the named stage and checking
the "Set to this stage" checkbox.`,
},
set_stage: {
name: 'active_stage_id',
type: 'checkbox',

Просмотреть файл

@ -1,17 +1,18 @@
// This file contains helper functions for our elements.
import {html, nothing} from 'lit';
import {Feature, FeatureLink, StageDict} from '../js-src/cs-client.js';
import {markupAutolinks} from './autolink.js';
import {nothing, html} from 'lit';
import {FORMS_BY_STAGE_TYPE, FormattedFeature} from './form-definition.js';
import {
STAGE_FIELD_NAME_MAPPING,
PLATFORMS_DISPLAYNAME,
STAGE_SPECIFIC_FIELDS,
OT_MILESTONE_END_FIELDS,
ENTERPRISE_FEATURE_CATEGORIES_DISPLAYNAME,
ROLLOUT_IMPACT_DISPLAYNAME,
ENTERPRISE_IMPACT_DISPLAYNAME,
OT_MILESTONE_END_FIELDS,
PLATFORMS_DISPLAYNAME,
ROLLOUT_IMPACT_DISPLAYNAME,
STAGE_FIELD_NAME_MAPPING,
STAGE_SPECIFIC_FIELDS,
} from './form-field-enums';
import {FeatureLink} from '../js-src/cs-client.js';
let toastEl;
@ -101,6 +102,37 @@ export function findFirstFeatureStage(intentStage, currentStage, fe) {
return null;
}
/**
* Returns `stage`'s name, using either its `display_name` or a counter to disambiguate from other
* stages of the same type within `feature`.
*/
export function unambiguousStageName(
stage: StageDict,
feature: Feature | FormattedFeature
): string | undefined {
const processStageName = FORMS_BY_STAGE_TYPE[stage.stage_type]?.name;
if (!processStageName) {
console.error(
`Unexpected stage type ${stage.stage_type} in stage ${stage.id}.`
);
return undefined;
}
if (stage.display_name) {
return `${processStageName}: ${stage.display_name}`;
}
// Count the stages of the same type that appear before this one. This is O(n^2) when it's used to
// number every stage, but the total number of stages is generally <20.
const index = feature.stages
.filter(s => s.stage_type === stage.stage_type)
.findIndex(s => s.id === stage.id);
if (index > 0) {
return `${processStageName} ${index + 1}`;
}
// Ignore if the stage wasn't found.
return processStageName;
}
/* Get the value of a stage field using a form-specific name */
export function getStageValue(stage, fieldName) {
if (!stage) return undefined;
@ -172,12 +204,16 @@ export function hasFieldValue(fieldName, feStage, feature) {
* Note: This is independent of any value that might be in a corresponding
* form field.
*
* @param {string} fieldName - The name of the field to retrieve.
* @param {string} feStage - The stage of the feature.
* @param {Object} feature - The feature object to retrieve the field value from.
* @return {*} The value of the specified field for the given feature.
* @param fieldName - The name of the field to retrieve.
* @param feStage - The stage of the feature.
* @param feature - The feature object to retrieve the field value from.
* @return The value of the specified field for the given feature.
*/
export function getFieldValueFromFeature(fieldName, feStage, feature) {
export function getFieldValueFromFeature(
fieldName: string,
feStage: string,
feature: Feature
) {
if (STAGE_SPECIFIC_FIELDS.has(fieldName)) {
const value = getStageValue(feStage, fieldName);
if (fieldName === 'rollout_impact' && value) {
@ -245,6 +281,14 @@ export function getFieldValueFromFeature(fieldName, feStage, feature) {
if (fieldName === 'enterprise_impact' && value) {
return ENTERPRISE_IMPACT_DISPLAYNAME[value];
}
if (fieldName === 'active_stage_id' && value) {
for (const stage of feature.stages) {
if (stage.id === value) {
return unambiguousStageName(stage, feature);
}
}
return undefined;
}
return value;
}

Просмотреть файл

@ -77,3 +77,29 @@ test('add an origin trial stage', async ({ page }) => {
mask: [page.locator('section[id="history"]')]
});
});
test('set the active stage', async ({page}) => {
await createNewFeature(page);
// Edit the metadata.
const metadataSection = page.locator('sl-details[summary="Metadata"]');
await metadataSection.click();
await metadataSection.getByRole('link', {name: 'Edit fields'}).click();
// Select the origin trial stage.
const activeStageSelect = page.locator('sl-select[name="active_stage_id"]');
await activeStageSelect.click();
await activeStageSelect
.locator('sl-option', {hasText: 'Origin Trial'})
.click();
// Save.
await page.getByRole('button', {name: 'Submit'}).click();
// Check the origin trial is active.
await expect(
page
.locator('sl-details')
.getByRole('button', {name: 'Origin Trial - Active', expanded: true})
).toBeVisible();
});