Pull intent template from server endpoint

This commit is contained in:
Daniel Smith 2024-07-11 16:26:42 -07:00
Родитель 70ed6f2437
Коммит 1d03e0cece
7 изменённых файлов: 154 добавлений и 1368 удалений

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

@ -14,10 +14,14 @@
from typing import TypedDict
from flask import render_template
from api import converters
from framework import basehandlers
from framework import cloud_tasks_helpers
from framework import permissions
from internals import processes
from internals import stage_helpers
from internals.core_enums import INTENT_STAGES_BY_STAGE_TYPE
from internals.core_models import FeatureEntry, Stage
from internals.review_models import Gate
@ -36,6 +40,45 @@ class IntentOptions(TypedDict):
class IntentsAPI(basehandlers.APIHandler):
def do_get(self, **kwargs):
"""Get the body of a draft intent."""
feature_id = int(kwargs['feature_id'])
# Check that feature ID is valid.
if not feature_id:
self.abort(404, msg='No feature specified.')
feature: FeatureEntry|None = FeatureEntry.get_by_id(feature_id)
if feature is None:
self.abort(404, msg=f'Feature {feature_id} not found')
# Check that stage ID is valid.
stage_id = int(kwargs['stage_id'])
if not stage_id:
self.abort(404, msg='No stage specified')
stage: Stage|None = Stage.get_by_id(stage_id)
if stage is None:
self.abort(404, msg=f'Stage {stage_id} not found')
# Check that the user has feature edit permissions.
redirect_resp = permissions.validate_feature_edit_permission(
self, feature_id)
if redirect_resp:
return redirect_resp
stage_info = stage_helpers.get_stage_info_for_templates(feature)
intent_stage = INTENT_STAGES_BY_STAGE_TYPE[stage.stage_type]
default_url = (f'{self.request.scheme}://{self.request.host}'
f'/feature/{feature_id}')
template_data = {
'feature': converters.feature_entry_to_json_verbose(feature),
'stage_info': stage_helpers.get_stage_info_for_templates(feature),
'should_render_mstone_table': stage_info['should_render_mstone_table'],
'should_render_intents': stage_info['should_render_intents'],
'intent_stage': intent_stage,
'default_url': default_url,
}
return render_template('blink/intent_to_implement.html', **template_data)
def do_post(self, **kwargs):
"""Submit an intent email directly to blink-dev."""
feature_id = int(kwargs['feature_id'])
@ -48,7 +91,7 @@ class IntentsAPI(basehandlers.APIHandler):
body = self.get_json_param_dict()
# Check that stage ID is valid.
stage_id = body.get('stage_id')
stage_id = int(kwargs['stage_id'])
if not stage_id:
self.abort(404, msg='No stage specified')
stage: Stage|None = Stage.get_by_id(stage_id)

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

@ -3,6 +3,9 @@ import {SHARED_STYLES} from '../css/shared-css.js';
import {showToastMessage} from './utils';
import {openPostIntentDialog} from './chromedash-post-intent-dialog.js';
import {
GATE_TYPES,
FEATURE_TYPES,
INTENT_STAGES,
STAGE_TYPES_DEV_TRIAL,
STAGE_TYPES_SHIPPING,
} from './form-field-enums.js';
@ -18,6 +21,8 @@ class ChromedashGuideIntentPreview extends LitElement {
stage: {type: Object},
gate: {type: Object},
loading: {type: Boolean},
subject: {type:String},
intentBody: {type: String},
displayFeatureUnlistedWarning: {type: Boolean},
};
}
@ -31,6 +36,8 @@ class ChromedashGuideIntentPreview extends LitElement {
this.stage = undefined;
this.gate = undefined;
this.loading = true;
this.subject = '';
this.intentBody = '';
this.displayFeatureUnlistedWarning = false;
}
@ -79,6 +86,7 @@ class ChromedashGuideIntentPreview extends LitElement {
])
.then(([feature, gates]) => {
this.feature = feature;
document.title = `${this.feature.name} - ${this.appTitle}`;
// TODO(DanielRyanSmith): only fetch a single gate based on given ID.
if (this.gateId) {
this.gate = gates.gates.find(gate => gate.id === this.gateId);
@ -94,12 +102,15 @@ class ChromedashGuideIntentPreview extends LitElement {
);
}
if (this.feature.name) {
document.title = `${this.feature.name} - ${this.appTitle}`;
}
if (this.feature.unlisted) {
this.displayFeatureUnlistedWarning = true;
}
this.subject = `${this.computeSubjectPrefix()}: ${this.feature.name}`;
// Finally, get the contents of the intent based on the feature/stage.
return window.csClient.getIntentBody(this.featureId, this.stage.id);
})
.then(intentBody => {
this.intentBody = intentBody;
this.loading = false;
})
.catch(() => {
@ -132,9 +143,79 @@ class ChromedashGuideIntentPreview extends LitElement {
</div>`;
}
computeSubjectPrefix() {
// DevTrials don't have a gate associated with their stage.
if (!this.gate) {
return 'Ready for Developer Testing';
}
if (
this.gate.gate_type === GATE_TYPES.API_PROTOTYPE ||
this.gate.gate_type === GATE_TYPES.API_PLAN
) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Intent to Deprecate and Remove';
}
return 'Intent to Prototype';
}
if (this.gate.gate_type === GATE_TYPES.API_ORIGIN_TRIAL) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Request for Deprecation Trial';
}
return 'Intent to Experiment';
}
if (this.gate.gate_type === GATE_TYPES.API_EXTEND_ORIGIN_TRIAL) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Intent to Extend Deprecation Trial';
}
return 'Intent to Extend Experiment';
}
if (this.gate.gate_type === GATE_TYPES.API_SHIP) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_CODE_CHANGE_ID[0]
) {
return 'Web-Facing Change PSA';
}
return 'Intent to Ship';
}
return `Intent stage "${INTENT_STAGES[this.feature.intent_stage]}"`;
}
renderSkeletonSection() {
return html`
<section>
<h3><sl-skeleton effect="sheen"></sl-skeleton></h3>
<p>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
</p>
</section>
`;
}
renderSkeletons() {
return html`
<div id="feature" style="margin-top: 65px;">
${this.renderSkeletonSection()} ${this.renderSkeletonSection()}
${this.renderSkeletonSection()} ${this.renderSkeletonSection()}
</div>
`;
}
render() {
if (!this.feature) return nothing;
if (this.loading) {
return this.renderSkeletons();
}
return html`
<div id="content">
<div id="subheader">
@ -171,7 +252,8 @@ class ChromedashGuideIntentPreview extends LitElement {
appTitle="${this.appTitle}"
.feature=${this.feature}
.stage=${this.stage}
.gate=${this.gate}
subject="${this.subject}"
intentBody="${this.intentBody}"
>
</chromedash-intent-template>
</section>

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

@ -1,37 +1,22 @@
import {LitElement, css, html, nothing} from 'lit';
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import {SHARED_STYLES} from '../css/shared-css.js';
import {
GATE_TYPES,
FEATURE_TYPES,
INTENT_STAGES,
STAGE_DEP_PLAN,
STAGE_TYPES_INTENT_EXPERIMENT,
STAGE_TYPES_ORIGIN_TRIAL,
STAGE_TYPES_PROTOTYPE,
STAGE_TYPES_SHIPPING,
OT_EXTENSION_STAGE_TYPES,
STAGE_BLINK_EVAL_READINESS,
STAGE_TYPES_DEV_TRIAL,
} from './form-field-enums.js';
import {showToastMessage} from './utils.js';
export class ChromedashIntentTemplate extends LitElement {
static get properties() {
return {
appTitle: {type: String},
feature: {type: Object},
stage: {type: Object},
gate: {type: Object},
displayFeatureUnlistedWarning: {type: Boolean},
subject: {type: String},
intentBody: {type: String},
};
}
constructor() {
super();
this.appTitle = '';
this.feature = {};
this.stage = undefined;
this.gate = undefined;
this.subject = '';
this.intentBody = '';
}
static get styles() {
@ -180,904 +165,12 @@ export class ChromedashIntentTemplate extends LitElement {
}
}
computeSubjectPrefix() {
// DevTrials don't have a gate associated with their stage.
if (!this.gate) {
return 'Ready for Developer Testing';
}
if (
this.gate.gate_type === GATE_TYPES.API_PROTOTYPE ||
this.gate.gate_type === GATE_TYPES.API_PLAN
) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Intent to Deprecate and Remove';
}
return 'Intent to Prototype';
}
if (this.gate.gate_type === GATE_TYPES.API_ORIGIN_TRIAL) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Request for Deprecation Trial';
}
return 'Intent to Experiment';
}
if (this.gate.gate_type === GATE_TYPES.API_EXTEND_ORIGIN_TRIAL) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]
) {
return 'Intent to Extend Deprecation Trial';
}
return 'Intent to Extend Experiment';
}
if (this.gate.gate_type === GATE_TYPES.API_SHIP) {
if (
this.feature.feature_type_int ===
FEATURE_TYPES.FEATURE_TYPE_CODE_CHANGE_ID[0]
) {
return 'Web-Facing Change PSA';
}
return 'Intent to Ship';
}
return `Intent stage "${INTENT_STAGES[this.feature.intent_stage]}"`;
}
renderOwners() {
const owners = this.feature.owner_emails;
let ownersHTML = html`None`;
if (owners) {
ownersHTML = owners.map((o, i) => {
if (i + 1 === owners.length) {
return html`<a href="mailto:${o}">${o}</a>`;
}
return html`<a href="mailto:${o}">${o}</a>, `;
});
}
return html`<h4>Contact emails</h4>
${ownersHTML}`;
}
renderExplainerLinks() {
const explainerLinks = this.feature.explainer_links;
if (this.feature.feature_type_int === 2) {
return nothing;
}
let explainerLinksHTML = html`None`;
if (explainerLinks && explainerLinks.length > 0) {
explainerLinksHTML = explainerLinks.map((link, i) => {
if (i === 0) {
return html`<a href="${link}">${link}</a>`;
}
return html`<br /><a href="${link}">${link}</a>`;
});
}
return html`<br /><br />
<h4>Explainer</h4>
${explainerLinksHTML}`;
}
renderSpecification() {
const spec = this.feature.standards?.spec;
let specHTML = html`None`;
if (spec) {
specHTML = html`<a href="${spec}">${spec}</a>`;
}
return html`<br /><br />
<h4>Specification</h4>
${specHTML}`;
}
renderDesignDocs() {
const docs = this.feature.resources?.docs;
if (!docs || docs.length === 0) return nothing;
let docsHTML = html`None`;
docsHTML = docs.map((link, i) => {
if (i === 0) {
return html`<a href="${link}">${link}</a>`;
}
return html`<br /><a href="${link}">${link}</a>`;
});
return html` <br /><br />
<h4>Design docs</h4>
${docsHTML}`;
}
renderSummary() {
const summary = this.feature.summary;
let summaryHTML = html`None`;
if (summary) {
summaryHTML = html`<p>${this.feature.summary}</p>`;
}
return html` <br /><br />
<h4>Summary</h4>
${summaryHTML}`;
}
renderBlinkComponents() {
const blinkComponents = this.feature.blink_components;
let blinkComponentsHTML = html`None`;
if (blinkComponents && blinkComponents.length > 0) {
blinkComponentsHTML = blinkComponents.map(
bc =>
html`<a
href="https://bugs.chromium.org/p/chromium/issues/list?q=component:${bc}"
target="_blank"
rel="noopener"
>${bc}</a
>`
);
}
return html`
<br /><br />
<h4>Blink component</h4>
${blinkComponentsHTML}
`;
}
renderMotivation() {
if (
!STAGE_TYPES_PROTOTYPE.has(this.stage?.stage_type) &&
this.stage?.stage_type !== STAGE_DEP_PLAN
)
return nothing;
const motivation = this.feature.motivation || 'None';
return html`
<br /><br />
<h4>Motivation</h4>
<p>${motivation}</p>
`;
}
renderInitialPublicProposal() {
if (
!STAGE_TYPES_PROTOTYPE.has(this.stage?.stage_type) &&
this.stage?.stage_type !== STAGE_DEP_PLAN
)
return nothing;
const initialPublicProposalUrl =
this.feature.initial_public_proposal_url || 'None';
return html`
<br /><br />
<h4>Initial public proposal</h4>
<a href="${initialPublicProposalUrl}">${initialPublicProposalUrl}</a>
`;
}
renderTags() {
const tags = this.feature.tags;
if (!tags || tags.length === 0) return nothing;
const tagsHTML = tags.map((t, i) => {
if (i + 1 === tags.length) {
return html`<a href="/features#tags:${t}">${t}</a>`;
}
return html`<a href="/features#tags:${t}">${t}</a>, `;
});
return html`
<br /><br />
<h4>Search tags</h4>
${tagsHTML}
`;
}
renderTagReview() {
const parts = [
html` <br /><br />
<h4>TAG review</h4>
${this.feature.tag_review || 'None'}`,
];
const tagReviewStatus = this.feature.tag_review_status;
if (tagReviewStatus) {
parts.push(
html` <br /><br />
<h4>TAG review status</h4>
${this.feature.tag_review_status}`
);
}
return html`${parts}`;
}
renderOTInfo(otStages) {
if (otStages.length === 0) return nothing;
const parts = [];
otStages.forEach((s, i) => {
const stageParts = [];
if (s.ot_chromium_trial_name) {
stageParts.push(
html` <br /><br />
<h4>Chromium Trial Name</h4>
${s.ot_chromium_trial_name}`
);
}
if (s.origin_trial_feedback_url) {
stageParts.push(
html` <br /><br />
<h4>Link to origin trial feedback summary</h4>
${s.origin_trial_feedback_url}`
);
}
if (s.ot_is_deprecation_trial && s.ot_webfeature_use_counter) {
stageParts.push(
html` <br /><br />
<h4>WebFeature UseCounter name</h4>
${s.ot_webfeature_use_counter}`
);
}
if (stageParts.length > 0 && s.ot_display_name) {
stageParts.shift(
html` <br /><br />
<h4>
<strong>Origin Trial</strong> ${i + 1}: ${s.ot_display_name}
</h4>`
);
}
parts.push(...stageParts);
});
return html`${parts}`;
}
renderRisks() {
const parts = [];
// Interop risks
const interopRisks = this.feature.interop_compat_risks;
let interopRisksHTML = html`None`;
if (interopRisks) {
interopRisksHTML = html`<p>${interopRisks}</p>`;
}
parts.push(
html` <br /><br />
<h4>Interoperability and Compatibility</h4>
${interopRisksHTML}`
);
// Gecko risks
parts.push(
html` <br /><br /><i>Gecko:</i> ${this.feature.browsers.ff.view.text ||
html`None`}`
);
if (this.feature.browsers.ff.view.url) {
parts.push(html`
(<a href="${this.feature.browsers.ff.view.url}"
>${this.feature.browsers.ff.view.url}</a
>)
`);
}
const geckoNotes = this.feature.browsers.ff.view.notes;
if (geckoNotes) {
parts.push(geckoNotes);
}
// WebKit risks
parts.push(
html` <br /><br /><i>WebKit:</i> ${this.feature.browsers.safari.view
.text || html`None`}`
);
if (this.feature.browsers.safari.view.url) {
parts.push(html`
(<a href="${this.feature.browsers.safari.view.url}"
>${this.feature.browsers.safari.view.url}</a
>)
`);
}
const webKitNotes = this.feature.browsers.safari.view.notes;
if (webKitNotes) {
parts.push(webKitNotes);
}
// Web developer risks
parts.push(
html` <br /><br /><i>Web developers</i>:
${this.feature.browsers.webdev.view.text || html`None`}`
);
if (this.feature.browsers.webdev.view.url) {
parts.push(html`
(<a href="${this.feature.browsers.webdev.view.url}"
>${this.feature.browsers.webdev.view.url}</a
>)
`);
}
const webdevNotes = this.feature.browsers.webdev.view.notes;
if (webdevNotes) {
parts.push(webdevNotes);
}
parts.push(html` <br /><br /><i>Other signals</i>: `);
if (this.feature.browsers.other.view.notes) {
parts.push(html`${this.feature.browsers.other.view.notes}`);
}
if (this.feature.ergonomics_risks) {
parts.push(
html` <br /><br />
<h4>Ergonomics</h4>
<p>${this.feature.ergonomics_risks}</p>`
);
}
if (this.feature.activation_risks) {
parts.push(
html` <br /><br />
<h4>Activation</h4>
<p>${this.feature.activation_risks}</p>`
);
}
if (this.feature.security_risks) {
parts.push(
html` <br /><br />
<h4>Security</h4>
<p>${this.feature.security_risks}</p>`
);
}
parts.push(
html` <br /><br />
<h4>WebView application risks</h4>
<p style="font-style: italic">
Does this intent deprecate or change behavior of existing APIs, such
that it has potentially high risk for Android WebView-based
applications?
</p>
<p>${this.feature.webview_risks || html`None`}</p>`
);
return html` <br /><br />
<h4>Risks</h4>
<div style="margin-left: 4em;">${parts}</div>`;
}
renderExperimentGoals() {
// Only show this section for experiment intents.
if (
this.stage &&
!STAGE_TYPES_INTENT_EXPERIMENT.has(this.stage?.stage_type)
)
return nothing;
const parts = [
html` <br /><br />
<h4>Goals for experimentation</h4>
<p>${this.feature.experiment_goals || 'None'}</p>`,
];
if (this.feature.experiment_timeline) {
parts.push(
html` <br /><br />
<h4>Experiment timeline</h4>
<p>${this.feature.experiment_timeline}</p>`
);
}
if (
OT_EXTENSION_STAGE_TYPES.has(this.stage?.stage_type) &&
this.stage.experiment_extension_reason
) {
parts.push(
html` <br /><br />
<h4>Reason this experiment is being extended</h4>
<p>${stage.experiment_extension_reason}</p>`
);
}
parts.push(
html` <br /><br />
<h4>Ongoing technical constraints</h4>
${this.feature.ongoing_constraints || 'None'}`
);
return html`${parts}`;
}
renderDebuggability() {
return html` <br /><br />
<h4>Debuggability</h4>
<p>${this.feature.debuggability || 'None'}</p>`;
}
renderAllPlatforms() {
// This section is only shown for experimental and shipping intents.
if (
this.stage &&
!STAGE_TYPES_INTENT_EXPERIMENT.has(this.stage?.stage_type) &&
!STAGE_TYPES_SHIPPING.has(this.stage?.stage_type)
)
return nothing;
let descriptionHTML = nothing;
if (this.feature.all_platforms_descr) {
descriptionHTML = html`<p>${this.feature.all_platforms_descr}</p>`;
}
return html` <br /><br />
<h4>
Will this feature be supported on all six Blink platforms (Windows, Mac,
Linux, ChromeOS, Android, and Android WebView)?
</h4>
${this.feature.all_platforms ? 'Yes' : 'No'}<br />
${descriptionHTML}`;
}
renderWPT() {
let descriptionHTML = nothing;
if (this.feature.wpt_descr) {
descriptionHTML = html`<br />
<p>${this.feature.wpt_descr}</p>`;
}
return html` <br /><br />
<h4>
Is this feature fully tested by
<a
href="https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md"
>web-platform-tests</a
>?
</h4>
${this.feature.wpt ? 'Yes' : 'No'} ${descriptionHTML}`;
}
renderDevTrialInstructions() {
if (!this.feature.devtrial_instructions) return nothing;
return html` <br /><br />
<h4>DevTrial instructions</h4>
<a href="${this.feature.devtrial_instructions}"
>${this.feature.devtrial_instructions}</a
>`;
}
renderFlagName() {
return html` <br /><br />
<h4>Flag name on chrome://flags</h4>
${this.feature.flag_name || 'None'}`;
}
renderFinchInfo() {
const parts = [
html` <br /><br />
<h4>Finch feature name</h4>
${this.feature.finch_name || 'None'}`,
];
let nonFinchJustificationHTML = html`None`;
if (this.feature.non_finch_justification) {
nonFinchJustificationHTML = html` <p>
${this.feature.non_finch_justification}
</p>`;
}
parts.push(
html` <br /><br />
<h4>Non-finch justification</h4>
${nonFinchJustificationHTML}`
);
return html`${parts}`;
}
renderEmbedderSupport() {
return html` <br /><br />
<h4>Requires code in //chrome?</h4>
${this.feature.requires_embedder_support ? 'Yes' : 'No'}`;
}
renderTrackingBug() {
if (!this.feature.browsers.chrome.bug) return nothing;
return html` <br /><br />
<h4>Tracking bug</h4>
<a href="${this.feature.browsers.chrome.bug}"
>${this.feature.browsers.chrome.bug}</a
>`;
}
renderLaunchBug() {
if (!this.feature.launch_bug_url) return nothing;
return html` <br /><br />
<h4>Launch bug</h4>
<a href="${this.feature.launch_bug_url}"
>${this.feature.launch_bug_url}</a
>`;
}
renderMeasurement() {
if (!this.feature.measurement) return nothing;
return html` <br /><br />
<h4>Measurement</h4>
${this.feature.measurement}`;
}
renderAvailabilityExpectation() {
if (!this.feature.availability_expectation) return nothing;
return html` <br /><br />
<h4>Availability expectation</h4>
${this.feature.availability_expectation}`;
}
renderAdoptionExpectation() {
if (!this.feature.adoption_expectation) return nothing;
return html` <br /><br />
<h4>Adoption expectation</h4>
${this.feature.adoption_expectation}`;
}
renderAdoptionPlan() {
if (!this.feature.adoption_plan) return nothing;
return html` <br /><br />
<h4>Adoption plan</h4>
${this.feature.adoption_plan}`;
}
renderNonOSSDeps() {
if (!this.feature.non_oss_deps) return nothing;
return html` <br /><br />
<h4>Non-OSS dependencies</h4>
<p style="font-style: italic">
Does the feature depend on any code or APIs outside the Chromium open
source repository and its open-source dependencies to function?
</p>
${this.feature.non_oss_deps}`;
}
renderSampleLinks() {
const samples = this.feature.resources?.samples || [];
// Only show for shipping stages.
if (
(!STAGE_TYPES_SHIPPING.has(this.stage?.stage_type) &&
!this.stage?.stage_type !== STAGE_BLINK_EVAL_READINESS) ||
samples.length === 0
) {
return nothing;
}
return html` <br /><br />
<h4>Sample links</h4>
${samples.map(url => html`<br /><a href="${url}">${url}</a>`)}`;
}
renderDesktopMilestonesTable(dtStages, otStages, shipStages) {
const shipStagesHTML = shipStages.map(s => {
if (!s.desktop_first) {
return nothing;
}
return html` <tr>
<td>Shipping on desktop</td>
<td>${s.desktop_first}</td>
</tr>`;
});
const otStagesHTML = otStages.map((s, i) => {
const parts = [];
const identifier = otStages.length > 1 ? `${i + 1} ` : '';
if (s.desktop_first) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}desktop first</td>
<td>${s.desktop_first}</td>
</tr>`
);
}
if (s.desktop_last) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}desktop last</td>
<td>${s.desktop_last}</td>
</tr>`
);
}
s.extensions.forEach((es, j) => {
const extensionIdentifier = s.extensions.length > 1 ? `${j + 1} ` : '';
if (es.desktop_last) {
parts.push(
html` <tr>
<td>
Origin trial ${identifier}extension ${extensionIdentifier}end
milestone
</td>
<td>${es.desktop_last}</td>
</tr>`
);
}
});
return html`${parts}`;
});
const dtStagesHTML = dtStages.map(s => {
if (!s.desktop_first) {
return nothing;
}
return html` <tr>
<td>DevTrial on desktop</td>
<td>${s.desktop_first}</td>
</tr>`;
});
return html` <table>
${shipStagesHTML}${otStagesHTML}${dtStagesHTML}
</table>`;
}
renderAndroidMilestonesTable(dtStages, otStages, shipStages) {
const shipStagesHTML = shipStages.map(s => {
if (!s.android_first) {
return nothing;
}
return html` <tr>
<td>Shipping on Android</td>
<td>${s.android_first}</td>
</tr>`;
});
const otStagesHTML = otStages.map((s, i) => {
const parts = [];
const identifier = otStages.length > 1 ? `${i + 1} ` : '';
if (s.android_first) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}Android first</td>
<td>${s.android_first}</td>
</tr>`
);
}
if (s.android_last) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}Android last</td>
<td>${s.android_last}</td>
</tr>`
);
}
return html`${parts}`;
});
const dtStagesHTML = dtStages.map(s => {
if (!s.android_first) {
return nothing;
}
return html` <tr>
<td>DevTrial on Android</td>
<td>${s.android_first}</td>
</tr>`;
});
return html` <table>
${shipStagesHTML}${otStagesHTML}${dtStagesHTML}
</table>`;
}
renderWebViewMilestonesTable(otStages, shipStages) {
const shipStagesHTML = shipStages.map(s => {
if (!s.webview_first) {
return nothing;
}
return html` <tr>
<td>Shipping on WebView</td>
<td>${s.webview_first}</td>
</tr>`;
});
const otStagesHTML = otStages.map((s, i) => {
const parts = [];
const identifier = otStages.length > 1 ? `${i + 1} ` : '';
if (s.webview_first) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}WebView first</td>
<td>${s.webview_first}</td>
</tr>`
);
}
if (s.webview_last) {
parts.push(
html` <tr>
<td>Origin trial ${identifier}WebView last</td>
<td>${s.webview_last}</td>
</tr>`
);
}
return html`${parts}`;
});
return html` <table>
${shipStagesHTML}${otStagesHTML}
</table>`;
}
renderIOSMilestonesTable(dtStages, shipStages) {
const shipStagesHTML = shipStages.map(s => {
if (!s.ios_first) {
return nothing;
}
return html` <tr>
<td>Shipping on iOS</td>
<td>${s.ios_first}</td>
</tr>`;
});
const dtStagesHTML = dtStages.map(s => {
if (!s.ios_first) {
return nothing;
}
return html` <tr>
<td>DevTrial on iOS</td>
<td>${s.ios_first}</td>
</tr>`;
});
return html` <table>
${shipStagesHTML}${dtStagesHTML}
</table>`;
}
renderEstimatedMilestones(dtStages, otStages, shipStages) {
// Don't display the table if no milestones are defined.
if (
!(
shipStages.some(
s =>
s.desktop_first || s.android_first || s.ios_first || s.webview_first
) ||
otStages.some(
s =>
s.desktop_first ||
s.android_first ||
s.webview_first ||
s.desktop_last ||
s.android_last ||
s.webview_last
) ||
dtStages.some(s => s.desktop_first || s.android_first || s.ios_first)
)
) {
return html` <br /><br />
<h4>Estimated milestones</h4>
<p>No milestones specified</p>`;
}
return html` <br /><br />
<h4>Estimated milestones</h4>
${this.renderDesktopMilestonesTable(dtStages, otStages, shipStages)}
${this.renderAndroidMilestonesTable(dtStages, otStages, shipStages)}
${this.renderWebViewMilestonesTable(otStages, shipStages)}
${this.renderIOSMilestonesTable(dtStages, shipStages)}`;
}
renderAnticipatedSpecChanges() {
// Only show for shipping stages or if the anticipated spec changes
// field is filled.
if (
!STAGE_TYPES_SHIPPING.has(this.stage?.stage_type) &&
!this.feature.anticipated_spec_changes
)
return nothing;
const anticipatedSpecChanges =
this.feature.anticipated_spec_changes || 'None';
return html` <br /><br />
<h4>Anticipated spec changes</h4>
<p style="font-style: italic">
Open questions about a feature may be a source of future web compat or
interop issues. Please list open issues (e.g. links to known github
issues in the project for the feature specification) whose resolution
may introduce web compat/interop risk (e.g., changing to naming or
structure of the API in a non-backward-compatible way).
</p>
${anticipatedSpecChanges}`;
}
renderChromestatusLink() {
let urlSuffix;
if (this.gate) {
urlSuffix = `feature/${this.feature.id}?gate=${this.gate.id}`;
} else {
urlSuffix = `feature/${this.feature.id}`;
}
const url = `${window.location.protocol}//${window.location.host}/${urlSuffix}`;
return html` <br /><br />
<h4>Link to entry on ${this.appTitle}</h4>
<a href="${url}">${url}</a>`;
}
renderIntents(protoStages, dtStages, otStages, shipStages) {
const parts = [];
protoStages.forEach(s => {
if (s.intent_thread_url) {
parts.push(
html` Intent to Prototype:
<a href="${s.intent_thread_url}">${s.intent_thread_url}</a> <br />`
);
}
});
dtStages.forEach(s => {
if (s.announcement_url) {
parts.push(
html`Ready for Trial:
<a href="${s.announcement_url}">${s.announcement_url}</a> <br />`
);
}
});
otStages.forEach((s, i) => {
const identifier = otStages.length > 1 ? ` ${i + 1}` : '';
if (s.intent_thread_url) {
parts.push(
html`Intent to Experiment${identifier}:
<a href="${s.intent_thread_url}">${s.intent_thread_url}</a> <br />`
);
}
s.extensions?.forEach((es, j) => {
const extensionIdentifier =
s.extensions.length > 1 ? ` (Extension ${j + 1})` : '';
if (es.intent_thread_url) {
parts.push(
html` Intent to Extend Experiment${extensionIdentifier}:
<a href="${es.intent_thread_url}">${es.intent_thread_url}</a>
<br />`
);
}
});
});
shipStages.forEach(s => {
if (s.intent_thread_url) {
parts.push(
html` Intent to Ship:
<a href="${s.intent_thread_url}">${s.intent_thread_url}</a> <br />`
);
}
});
if (parts.length === 0) return nothing;
return html` <br /><br />
<h4>Links to previous Intent discussions</h4>
${parts}`;
}
renderFooterNote() {
const url = `${window.location.protocol}//${window.location.host}/`;
return html` <br /><br />
<div>
<small
>This intent message was generated by
<a href="${url}">${this.appTitle}</a>.</small
>
</div>`;
}
renderEmailBody() {
const protoStages = this.feature.stages.filter(s =>
STAGE_TYPES_PROTOTYPE.has(s.stage_type)
);
const dtStages = this.feature.stages.filter(s =>
STAGE_TYPES_DEV_TRIAL.has(s.stage_type)
);
const otStages = this.feature.stages.filter(s =>
STAGE_TYPES_ORIGIN_TRIAL.has(s.stage_type)
);
const shipStages = this.feature.stages.filter(s =>
STAGE_TYPES_SHIPPING.has(s.stage_type)
);
return html`${[
this.renderOwners(),
this.renderExplainerLinks(),
this.renderSpecification(),
this.renderDesignDocs(),
this.renderSummary(),
this.renderBlinkComponents(),
this.renderMotivation(),
this.renderInitialPublicProposal(),
this.renderTags(),
this.renderTagReview(),
this.renderOTInfo(otStages),
this.renderRisks(),
this.renderExperimentGoals(),
this.renderDebuggability(),
this.renderAllPlatforms(),
this.renderWPT(),
this.renderDevTrialInstructions(),
this.renderFlagName(),
this.renderFinchInfo(),
this.renderEmbedderSupport(),
this.renderTrackingBug(),
this.renderLaunchBug(),
this.renderMeasurement(),
this.renderAvailabilityExpectation(),
this.renderAdoptionExpectation(),
this.renderAdoptionPlan(),
this.renderNonOSSDeps(),
this.renderSampleLinks(),
this.renderEstimatedMilestones(dtStages, otStages, shipStages),
this.renderAnticipatedSpecChanges(),
this.renderChromestatusLink(),
this.renderIntents(protoStages, dtStages, otStages, shipStages),
this.renderFooterNote(),
]}`;
if (this.intentBody) {
// Needed for rendering HTML format returned from the API.
return unsafeHTML(this.intentBody);
}
return nothing;
}
render() {
@ -1090,7 +183,7 @@ export class ChromedashIntentTemplate extends LitElement {
<p>Subject</p>
<div class="email-content-border">
<div class="subject email-content-div" id="email-subject-content">
${this.computeSubjectPrefix()}: ${this.feature.name}
${this.subject}
</div>
</div>
<p>

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

@ -3,116 +3,13 @@ import {assert, fixture} from '@open-wc/testing';
import {ChromedashIntentTemplate} from './chromedash-intent-template';
describe('chromedash-intent-template', () => {
const validFeature = {
id: 123456,
name: 'feature one',
summary: 'fake detailed summary',
category: 'fake category',
feature_type: 'fake feature type',
feature_type_int: 0,
new_crbug_url: 'fake crbug link',
owner_emails: ['owner1@example.com', 'owner2@example.com'],
explainer_links: [
'https://example.com/explainer1',
'https://example.com/explainer2',
],
blink_components: ['Blink'],
interop_compat_risks: 'Some risks here',
ongoing_constraints: 'Some constraints',
finch_name: 'AFinchName',
browsers: {
chrome: {
status: {
milestone_str: 'No active development',
text: 'No active development',
val: 1,
},
},
ff: {view: {text: 'No signal', val: 5}},
safari: {view: {text: 'No signal', val: 5}},
webdev: {view: {text: 'Positive', val: 1}},
other: {view: {}},
},
stages: [
{
id: 1,
stage_type: 110,
intent_stage: 1,
},
{
id: 2,
stage_type: 150,
intent_thread_url: 'https://example.com/experiment',
desktop_first: 100,
desktop_last: 106,
android_first: 100,
android_last: 106,
webview_first: 100,
webview_last: 106,
extensions: [
{
id: 22,
stage_type: 151,
intent_thread_url: 'https://example.com/extend',
desktop_last: 109,
},
],
},
{
id: 3,
stage_type: 160,
intent_thread_url: 'https://example.com/ship',
desktop_first: 110,
android_first: 110,
webview_first: 110,
},
{
id: 4,
stage_type: 160,
},
],
browsers: {
chrome: {
blink_components: ['Blink'],
owners: ['fake chrome owner one', 'fake chrome owner two'],
status: {
milestone_str: 'No active development',
text: 'No active development',
val: 1,
},
},
ff: {view: {text: 'No signal', val: 5}},
safari: {view: {text: 'No signal', val: 5}},
webdev: {view: {text: 'No signal', val: 4}},
other: {view: {}},
},
resources: {
samples: ['fake sample link one', 'fake sample link two'],
docs: ['fake doc link one', 'fake doc link two'],
},
standards: {
maturity: {
short_text: 'Incubation',
text: 'Specification being incubated in a Community Group',
val: 3,
},
status: {text: "Editor's Draft", val: 4},
},
tags: ['tag_one'],
};
const validGate = {
id: 200,
stage_id: 2,
gate_type: 2,
};
it('renders with fake data', async () => {
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[1]}
.gate=${validGate}
subject="A fake subject"
intentBody="<div>A basic intent body</div>"
>
</chromedash-intent-template>`
);
@ -123,332 +20,8 @@ describe('chromedash-intent-template', () => {
'#email-subject-content'
);
const body = component.shadowRoot.querySelector('#email-body-content');
const expectedBody = `Contact emails
owner1@example.com, owner2@example.com
Explainer
https://example.com/explainer1
https://example.com/explainer2
Specification
None
Design docs
fake doc link one
fake doc link two
Summary
fake detailed summary
Blink component
Blink
Search tags
tag_one
TAG review
None
Risks
Interoperability and Compatibility
Some risks here
Gecko: No signal
WebKit: No signal
Web developers: No signal
Other signals:
WebView application risks
Does this intent deprecate or change behavior of existing APIs, such that it has potentially high risk for Android WebView-based applications?
None
Goals for experimentation
None
Ongoing technical constraints
Some constraints
Debuggability
None
Will this feature be supported on all six Blink platforms (Windows, Mac, Linux, ChromeOS, Android, and Android WebView)?
No
Is this feature fully tested by web-platform-tests?
No
Flag name on chrome://flags
None
Finch feature name
AFinchName
Non-finch justification
None
Requires code in //chrome?
No
Estimated milestones
Shipping on desktop 110
Origin trial desktop first 100
Origin trial desktop last 106
Origin trial extension end milestone 109
Shipping on Android 110
Origin trial Android first 100
Origin trial Android last 106
Shipping on WebView 110
Origin trial WebView first 100
Origin trial WebView last 106
Link to entry on Chrome Status Test
http://localhost:8000/feature/123456?gate=200
Links to previous Intent discussions
Intent to Experiment: https://example.com/experiment
Intent to Extend Experiment: https://example.com/extend
Intent to Ship: https://example.com/ship
This intent message was generated by Chrome Status Test.`;
const expectedSubject = 'Intent to Experiment: feature one';
assert.equal(body.innerText, expectedBody);
assert.equal(subject.innerText, expectedSubject);
});
it('renders deprecation plan intent', async () => {
// Deprecation feature type.
validFeature.feature_type_int = 3;
// Deprecation plan gate type.
validGate.gate_type = 5;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Intent to Deprecate and Remove: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders prototype intent', async () => {
// New feature type.
validFeature.feature_type_int = 0;
// Prototype gate type.
validGate.gate_type = 1;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Intent to Prototype: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders "Ready for Developer Testing" template', async () => {
// New feature type.
validFeature.feature_type_int = 0;
// No gate or stage provided for DevTrial templates.
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Ready for Developer Testing: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders "Intent to Ship" template', async () => {
// New feature type.
validFeature.feature_type_int = 0;
// Ship gate type.
validGate.gate_type = 4;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Intent to Ship: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders "Intent to Extend Experiment" template', async () => {
// New feature type.
validFeature.feature_type_int = 0;
// OT extension gate type.
validGate.gate_type = 3;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[1].extensions[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Intent to Extend Experiment: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders "Request for Deprecation Trial" template', async () => {
// Deprecation feature type.
validFeature.feature_type_int = 3;
// OT gate type.
validGate.gate_type = 2;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[1].extensions[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Request for Deprecation Trial: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders "Intent to Extend Deprecation Trial" template', async () => {
// Deprecation feature type.
validFeature.feature_type_int = 3;
// OT extension gate type.
validGate.gate_type = 3;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[1].extensions[0]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Intent to Extend Deprecation Trial: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
});
it('renders PSA shipping template', async () => {
// PSA feature type.
validFeature.feature_type_int = 2;
// Shipping gate type.
validGate.gate_type = 4;
const component = await fixture(
html`<chromedash-intent-template
appTitle="Chrome Status Test"
.feature=${validFeature}
.stage=${validFeature.stages[2]}
.gate=${validGate}
>
</chromedash-intent-template>`
);
assert.exists(component);
assert.instanceOf(component, ChromedashIntentTemplate);
const expectedSubject = 'Web-Facing Change PSA: feature one';
const subject = component.shadowRoot.querySelector(
'#email-subject-content'
);
assert.equal(subject.innerText, expectedSubject);
assert.equal(body.innerText, 'A basic intent body');
assert.equal(subject.innerText, 'A fake subject');
});
});

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

@ -37,14 +37,6 @@ class ChromedashPostIntentDialog extends LitElement {
@property({type: Array<string>})
ownerEmails = [];
static get properties() {
return {};
}
constructor() {
super();
}
static get styles() {
return [
...SHARED_STYLES,
@ -101,7 +93,6 @@ class ChromedashPostIntentDialog extends LitElement {
}
submitIntent() {
console;
// Make sure that the CC emails input is valid.
const ccEmailsInput = this.shadowRoot!.querySelector('sl-input');
if (!ccEmailsInput || ccEmailsInput.hasAttribute('data-user-invalid')) {
@ -114,8 +105,7 @@ class ChromedashPostIntentDialog extends LitElement {
submitButton.setAttribute('disabled', '');
}
window.csClient
.postIntentToBlinkDev(this.featureId, {
stage_id: this.stageId,
.postIntentToBlinkDev(this.featureId, this.stageId, {
gate_id: this.gateId,
intent_cc_emails: ccEmailsInput?.value?.split(','),
})

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

@ -646,8 +646,11 @@ export class ChromeStatusClient {
}
// Intents API
async postIntentToBlinkDev(featureId, body) {
return this.doPost(`/features/${featureId}/postintent`, body);
async getIntentBody(featureId, stageId) {
return this.doGet(`/features/${featureId}/${stageId}/intent`)
}
async postIntentToBlinkDev(featureId, stageId, body) {
return this.doPost(`/features/${featureId}/${stageId}/intent`, body);
}
// Progress API

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

@ -137,6 +137,8 @@ api_routes: list[Route] = [
reviews_api.XfnGatesAPI),
Route(f'{API_BASE}/features/<int:feature_id>/postintent',
intents_api.IntentsAPI),
Route(f'{API_BASE}/features/<int:feature_id>/<int:stage_id>/intent',
intents_api.IntentsAPI),
Route(f'{API_BASE}/blinkcomponents',
blink_components_api.BlinkComponentsAPI),