Show and edit the active stage in the metadata section. (#4047)
This commit is contained in:
Родитель
401ff31f14
Коммит
55e6a2b8c0
|
@ -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();
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче