Convert guide/stage page into a component (#2163)
* Convert guide/stage page into a component * Fix python unit test
This commit is contained in:
Родитель
697cf9b38d
Коммит
bd92897383
|
@ -77,13 +77,13 @@ STAGE_FORMS = {
|
|||
|
||||
IMPL_STATUS_FORMS = {
|
||||
core_enums.INTENT_INCUBATE:
|
||||
(None, guideforms.ImplStatus_Incubate),
|
||||
('', guideforms.ImplStatus_Incubate),
|
||||
core_enums.INTENT_EXPERIMENT:
|
||||
(core_enums.BEHIND_A_FLAG, guideforms.ImplStatus_DevTrial),
|
||||
core_enums.INTENT_EXTEND_TRIAL:
|
||||
(core_enums.ORIGIN_TRIAL, guideforms.ImplStatus_OriginTrial),
|
||||
core_enums.INTENT_IMPLEMENT_SHIP:
|
||||
(None, guideforms.ImplStatus_EvalReadinessToShip),
|
||||
('', guideforms.ImplStatus_EvalReadinessToShip),
|
||||
core_enums.INTENT_SHIP:
|
||||
(core_enums.ENABLED_BY_DEFAULT, guideforms.ImplStatus_AllMilestones),
|
||||
core_enums.INTENT_SHIPPED:
|
||||
|
@ -256,43 +256,32 @@ class FeatureEditStage(basehandlers.FlaskHandler):
|
|||
|
||||
f, feature_process = self.get_feature_and_process(feature_id)
|
||||
|
||||
stage_name = ''
|
||||
for stage in feature_process.stages:
|
||||
if stage.outgoing_stage == stage_id:
|
||||
stage_name = stage.name
|
||||
|
||||
template_data = {
|
||||
'stage_name': stage_name,
|
||||
'stage_id': stage_id,
|
||||
}
|
||||
|
||||
# TODO(jrobbins): show useful error if stage not found.
|
||||
detail_form_class = STAGE_FORMS[f.feature_type][stage_id]
|
||||
|
||||
impl_status_offered, impl_status_form_class = IMPL_STATUS_FORMS.get(
|
||||
stage_id, (None, None))
|
||||
stage_id, ('', ''))
|
||||
|
||||
feature_edit_dict = f.format_for_edit()
|
||||
detail_form = None
|
||||
detail_form = ''
|
||||
if detail_form_class:
|
||||
detail_form = detail_form_class(feature_edit_dict)
|
||||
impl_status_form = None
|
||||
form = detail_form_class(feature_edit_dict)
|
||||
detail_form = json.dumps((str(form), list(form.fields)))
|
||||
impl_status_form = ''
|
||||
if impl_status_form_class:
|
||||
impl_status_form = impl_status_form_class(feature_edit_dict)
|
||||
form = impl_status_form_class(feature_edit_dict)
|
||||
impl_status_form = json.dumps((str(form), list(form.fields)))
|
||||
|
||||
# Provide new or populated form to template.
|
||||
template_data.update({
|
||||
'feature': f,
|
||||
template_data = {
|
||||
'stage_id': stage_id,
|
||||
'feature_id': f.key.integer_id(),
|
||||
'feature_form': detail_form,
|
||||
'already_on_this_stage': stage_id == f.intent_stage,
|
||||
'already_on_this_impl_status':
|
||||
impl_status_offered == f.impl_status_chrome,
|
||||
'impl_status_form': impl_status_form,
|
||||
'impl_status_name': core_enums.IMPLEMENTATION_STATUS.get(
|
||||
impl_status_offered, None),
|
||||
impl_status_offered, ''),
|
||||
'impl_status_offered': impl_status_offered,
|
||||
})
|
||||
}
|
||||
return template_data
|
||||
|
||||
@permissions.require_edit_feature
|
||||
|
|
|
@ -320,33 +320,8 @@ class FeatureEditStageTest(testing_config.CustomTestCase):
|
|||
with test_app.test_request_context(self.request_path):
|
||||
template_data = self.handler.get_template_data(
|
||||
self.feature_1.key.integer_id(), self.stage)
|
||||
|
||||
self.assertTrue('feature' in template_data)
|
||||
self.assertTrue('feature_id' in template_data)
|
||||
self.assertTrue('feature_form' in template_data)
|
||||
self.assertTrue('already_on_this_stage' in template_data)
|
||||
|
||||
def test_get__not_on_this_stage(self):
|
||||
"""When feature is not on the stage for the current form, offer checkbox."""
|
||||
testing_config.sign_in('user1@google.com', 1234567890)
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
template_data = self.handler.get_template_data(
|
||||
self.feature_1.key.integer_id(), self.stage)
|
||||
|
||||
self.assertFalse(template_data['already_on_this_stage'])
|
||||
|
||||
def test_get__already_on_this_stage(self):
|
||||
"""When feature is already on the stage for the current form, say that."""
|
||||
self.feature_1.intent_stage = self.stage
|
||||
self.feature_1.put()
|
||||
testing_config.sign_in('user1@google.com', 1234567890)
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
template_data = self.handler.get_template_data(
|
||||
self.feature_1.key.integer_id(), self.stage)
|
||||
|
||||
self.assertTrue(template_data['already_on_this_stage'])
|
||||
|
||||
def test_post__anon(self):
|
||||
"""Anon cannot edit features, gets a 403."""
|
||||
|
|
|
@ -49,6 +49,7 @@ import './elements/chromedash-guide-edit-page';
|
|||
import './elements/chromedash-guide-editall-page';
|
||||
import './elements/chromedash-guide-metadata';
|
||||
import './elements/chromedash-guide-new-page';
|
||||
import './elements/chromedash-guide-stage-page';
|
||||
import './elements/chromedash-guide-verify-accuracy-page';
|
||||
import './elements/chromedash-header';
|
||||
import './elements/chromedash-legend';
|
||||
|
|
|
@ -50,15 +50,16 @@ export class ChromedashFormField extends LitElement {
|
|||
const type = fieldProps.type;
|
||||
const choices = fieldProps.choices;
|
||||
|
||||
// If type is checkbox, then generate locally.
|
||||
// If type is checkbox, select, or input, then generate locally.
|
||||
let fieldHTML = '';
|
||||
if (type === 'checkbox') {
|
||||
// value can be a js or python boolean value converted to a string
|
||||
fieldHTML = html`
|
||||
<sl-checkbox
|
||||
name="${this.name}"
|
||||
id="id_${this.name}"
|
||||
size="small"
|
||||
?checked=${this.value === 'True' ? true : false}
|
||||
?checked=${this.value === 'true' || this.value === 'True'}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
${label}
|
||||
|
|
|
@ -24,19 +24,6 @@ export class ChromedashGuideEditPage extends LitElement {
|
|||
display: flex;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
sl-skeleton {
|
||||
margin-bottom: 1em;
|
||||
width: 60%;
|
||||
}
|
||||
sl-skeleton:nth-of-type(even) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
h3 sl-skeleton {
|
||||
width: 30%;
|
||||
height: 1.5em;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
|
|
|
@ -13,19 +13,6 @@ export class ChromedashGuideEditallPage extends LitElement {
|
|||
...SHARED_STYLES,
|
||||
...FORM_STYLES,
|
||||
css`
|
||||
sl-skeleton {
|
||||
margin-bottom: 1em;
|
||||
width: 60%;
|
||||
}
|
||||
sl-skeleton:nth-of-type(even) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
h3 sl-skeleton {
|
||||
margin-top: 1em;
|
||||
width: 30%;
|
||||
height: 1.25em;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
import {LitElement, css, html, nothing} from 'lit';
|
||||
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
|
||||
import {showToastMessage} from './utils.js';
|
||||
import './chromedash-form-table';
|
||||
import './chromedash-form-field';
|
||||
import {SHARED_STYLES} from '../sass/shared-css.js';
|
||||
import {FORM_STYLES} from '../sass/forms-css.js';
|
||||
|
||||
|
||||
export class ChromedashGuideStagePage extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
...FORM_STYLES,
|
||||
css`
|
||||
`];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stageId: {type: Number},
|
||||
stageName: {type: String},
|
||||
featureId: {type: Number},
|
||||
feature: {type: Object},
|
||||
featureForm: {type: String},
|
||||
implStatusForm: {type: String},
|
||||
implStatusName: {type: String},
|
||||
implStatusOffered: {type: String},
|
||||
loading: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.stageId = 0;
|
||||
this.stageName = '';
|
||||
this.featureId = 0;
|
||||
this.feature = {};
|
||||
this.featureForm = '';
|
||||
this.implStatusForm = '';
|
||||
this.implStatusName = '';
|
||||
this.implStatusOffered = '';
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
Promise.all([
|
||||
window.csClient.getFeature(this.featureId),
|
||||
window.csClient.getFeatureProcess(this.featureId),
|
||||
]).then(([feature, process]) => {
|
||||
this.feature = feature;
|
||||
process.stages.map(stage => {
|
||||
if (stage.outgoing_stage === this.stageId) {
|
||||
this.stageName = stage.name;
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
|
||||
// TODO(kevinshen56714): Remove this once SPA index page is set up.
|
||||
// Has to include this for now to remove the spinner at _base.html.
|
||||
document.body.classList.remove('loading');
|
||||
}).catch(() => {
|
||||
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
|
||||
});
|
||||
}
|
||||
|
||||
/* Add the form's event listener after Shoelace event listeners are attached
|
||||
* see more at https://github.com/GoogleChrome/chromium-dashboard/issues/2014 */
|
||||
firstUpdated() {
|
||||
/* TODO(kevinshen56714): remove the timeout once the form fields are all
|
||||
* migrated to frontend, we need it now because the unsafeHTML(this.overviewForm)
|
||||
* delays the Shoelace event listener attachment */
|
||||
setTimeout(() => {
|
||||
const hiddenTokenField = this.shadowRoot.querySelector('input[name=token]');
|
||||
hiddenTokenField.form.addEventListener('submit', (event) => {
|
||||
this.handleFormSubmission(event, hiddenTokenField);
|
||||
});
|
||||
this.addMiscEventListeners();
|
||||
this.scrollToPosition();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
handleFormSubmission(event, hiddenTokenField) {
|
||||
event.preventDefault();
|
||||
|
||||
// get the XSRF token and update it if it's expired before submission
|
||||
window.csClient.ensureTokenIsValid().then(() => {
|
||||
hiddenTokenField.value = window.csClient.token;
|
||||
event.target.submit();
|
||||
});
|
||||
}
|
||||
|
||||
addMiscEventListeners() {
|
||||
const fields = this.shadowRoot.querySelectorAll('input, textarea');
|
||||
for (let i = 0; i < fields.length; ++i) {
|
||||
fields[i].addEventListener('input', (e) => {
|
||||
e.target.classList.add('interacted');
|
||||
});
|
||||
}
|
||||
|
||||
// Allow editing if there was already a value specified in this
|
||||
// deprecated field.
|
||||
const timelineField = this.shadowRoot.querySelector('#id_experiment_timeline');
|
||||
if (timelineField && timelineField.value) {
|
||||
timelineField.disabled = '';
|
||||
}
|
||||
|
||||
// Copy field SRC to DST if SRC is edited and DST was empty and
|
||||
// has not been edited.
|
||||
const COPY_ON_EDIT = [
|
||||
['dt_milestone_desktop_start', 'dt_milestone_android_start'],
|
||||
['dt_milestone_desktop_start', 'dt_milestone_webview_start'],
|
||||
// Don't autofill dt_milestone_ios_start because it is rare.
|
||||
['ot_milestone_desktop_start', 'ot_milestone_android_start'],
|
||||
['ot_milestone_desktop_end', 'ot_milestone_android_end'],
|
||||
['ot_milestone_desktop_start', 'ot_milestone_webview_start'],
|
||||
['ot_milestone_desktop_end', 'ot_milestone_webview_end'],
|
||||
];
|
||||
|
||||
for (const [srcId, dstId] of COPY_ON_EDIT) {
|
||||
const srcEl = this.shadowRoot.querySelector('#id_' + srcId);
|
||||
const dstEl = this.shadowRoot.querySelector('#id_' + dstId);
|
||||
if (srcEl && dstEl && srcEl.value == dstEl.value) {
|
||||
srcEl.addEventListener('input', () => {
|
||||
if (!dstEl.classList.contains('interacted')) {
|
||||
dstEl.value = srcEl.value;
|
||||
dstEl.classList.add('copied');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToPosition() {
|
||||
if (location.hash) {
|
||||
const hash = decodeURIComponent(location.hash);
|
||||
if (hash) {
|
||||
const el = this.shadowRoot.querySelector(hash);
|
||||
el.scrollIntoView(true, {behavior: 'smooth'});
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleCancelClick() {
|
||||
window.location.href = `/guide/edit/${this.featureId}`;
|
||||
}
|
||||
|
||||
// get a comma-spearated list of field names
|
||||
getFormFields() {
|
||||
let fields = JSON.parse(this.featureForm)[1];
|
||||
|
||||
// if there is a implStatusForm. add its field names to the list
|
||||
if (this.implStatusForm) {
|
||||
fields = [...fields, ...JSON.parse(this.implStatusForm)[1]];
|
||||
}
|
||||
return fields.join();
|
||||
}
|
||||
|
||||
renderSkeletons() {
|
||||
return html`
|
||||
<h3><sl-skeleton effect="sheen"></sl-skeleton></h3>
|
||||
<section id="metadata">
|
||||
<h3><sl-skeleton effect="sheen"></sl-skeleton></h3>
|
||||
<p>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</p>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSubheader() {
|
||||
return html`
|
||||
<div id="subheader">
|
||||
<h2 id="breadcrumbs">
|
||||
<a href="/guide/edit/${this.featureId}">
|
||||
<iron-icon icon="chromestatus:arrow-back"></iron-icon>
|
||||
Edit feature: ${this.feature.name}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
<h3>${this.stageName}</h3>
|
||||
`;
|
||||
}
|
||||
|
||||
renderFeatureFormSection() {
|
||||
const alreadyOnThisStage = this.stageId === this.feature.intent_stage_int;
|
||||
return html`
|
||||
<section class="stage_form">
|
||||
<chromedash-form-table>
|
||||
${unsafeHTML(JSON.parse(this.featureForm)[0])}
|
||||
|
||||
<chromedash-form-field
|
||||
name="set_stage"
|
||||
stage=${this.stageName}
|
||||
value=${alreadyOnThisStage}
|
||||
?disabled=${alreadyOnThisStage}>
|
||||
</chromedash-form-field>
|
||||
|
||||
</chromedash-form-table>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderImplStatusFormSection() {
|
||||
const alreadyOnThisImplStatus = this.implStatusOffered === this.feature.impl_status_chrome;
|
||||
return html`
|
||||
<h3>Implementation in Chromium</h3>
|
||||
<section class="stage_form">
|
||||
<chromedash-form-table>
|
||||
${this.implStatusName ? html`
|
||||
<chromedash-form-field>
|
||||
<span slot="label">Implementation status:</span>
|
||||
|
||||
${alreadyOnThisImplStatus ?
|
||||
html`
|
||||
<span slot="help">
|
||||
This feature already has implementation status:
|
||||
<b>${this.implStatusName}</b>.
|
||||
</td>
|
||||
</span>
|
||||
` :
|
||||
// TODO(jrobbins): When checked, make some milestone fields required.
|
||||
html`
|
||||
<span slot="field">
|
||||
<input type="hidden" name="impl_status_offered"
|
||||
value=${this.implStatusOffered}>
|
||||
<input type="checkbox" name="set_impl_status"
|
||||
id="set_impl_status">
|
||||
<label for="set_impl_status">
|
||||
Set implementation status to: <b>${this.implStatusName}</b>
|
||||
</label>
|
||||
</span>
|
||||
<span slot="help">
|
||||
Check this box to update the implementation
|
||||
status of this feature in Chromium.
|
||||
</span>
|
||||
`}
|
||||
</chromedash-form-field>
|
||||
`: nothing}
|
||||
|
||||
${unsafeHTML(JSON.parse(this.implStatusForm)[0])}
|
||||
|
||||
</chromedash-form-table>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
return html`
|
||||
<form name="feature_form" method="POST"
|
||||
action="/guide/stage/${this.featureId}/${this.stageId}">
|
||||
<input type="hidden" name="token">
|
||||
<input type="hidden" name="form_fields" value=${this.getFormFields()} >
|
||||
|
||||
${this.renderFeatureFormSection()}
|
||||
|
||||
${this.implStatusName || this.implStatusForm ?
|
||||
this.renderImplStatusFormSection() : nothing}
|
||||
|
||||
<div class="final_buttons">
|
||||
<input class="button" type="submit" value="Submit">
|
||||
<button id="cancel-button" type="reset"
|
||||
@click=${this.handleCancelClick}>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.renderSubheader()}
|
||||
${this.loading ? this.renderSkeletons() : this.renderForm()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-guide-stage-page', ChromedashGuideStagePage);
|
|
@ -0,0 +1,146 @@
|
|||
import {html} from 'lit';
|
||||
import {assert, fixture} from '@open-wc/testing';
|
||||
import {ChromedashGuideStagePage} from './chromedash-guide-stage-page';
|
||||
import './chromedash-toast';
|
||||
import '../js-src/cs-client';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('chromedash-guide-stage-page', () => {
|
||||
const validFeaturePromise = Promise.resolve({
|
||||
id: 123456,
|
||||
name: 'feature one',
|
||||
summary: 'fake detailed summary',
|
||||
category: 'fake category',
|
||||
feature_type: 'fake feature type',
|
||||
intent_stage: 'fake intent stage',
|
||||
new_crbug_url: 'fake crbug link',
|
||||
browsers: {
|
||||
chrome: {
|
||||
blink_components: ['Blink'],
|
||||
owners: ['fake chrome owner one', 'fake chrome owner two'],
|
||||
status: {text: 'fake chrome status text'},
|
||||
},
|
||||
ff: {view: {text: 'fake ff view text'}},
|
||||
safari: {view: {text: 'fake safari view text'}},
|
||||
webdev: {view: {text: 'fake webdev view text'}},
|
||||
},
|
||||
resources: {
|
||||
samples: ['fake sample link one', 'fake sample link two'],
|
||||
docs: ['fake doc link one', 'fake doc link two'],
|
||||
},
|
||||
standards: {
|
||||
spec: 'fake spec link',
|
||||
maturity: {text: 'Unknown standards status - check spec link for status'},
|
||||
},
|
||||
tags: ['tag_one'],
|
||||
});
|
||||
const processPromise = Promise.resolve({
|
||||
stages: [{
|
||||
name: 'stage one',
|
||||
description: 'a description',
|
||||
progress_items: [],
|
||||
outgoing_stage: 1,
|
||||
actions: [],
|
||||
}],
|
||||
});
|
||||
const implStatusName = 'fake implStatusName';
|
||||
/* TODO: create a proper fake data once the form generation is migrated to frontend */
|
||||
const featureForm = '["", ["fake feature field 1", "fake feature field 2"]]';
|
||||
const implStatusForm = '["", ["fake implStatus field 1", "fake implStatus field 2"]]';
|
||||
|
||||
/* window.csClient and <chromedash-toast> are initialized at _base.html
|
||||
* which are not available here, so we initialize them before each test.
|
||||
* We also stub out the API calls here so that they return test data. */
|
||||
beforeEach(async () => {
|
||||
await fixture(html`<chromedash-toast></chromedash-toast>`);
|
||||
window.csClient = new ChromeStatusClient('fake_token', 1);
|
||||
sinon.stub(window.csClient, 'getFeature');
|
||||
sinon.stub(window.csClient, 'getFeatureProcess');
|
||||
window.csClient.getFeatureProcess.returns(processPromise);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.csClient.getFeature.restore();
|
||||
window.csClient.getFeatureProcess.restore();
|
||||
});
|
||||
|
||||
it('renders with no data', async () => {
|
||||
const invalidFeaturePromise = Promise.reject(new Error('Got error response from server'));
|
||||
window.csClient.getFeature.withArgs(0).returns(invalidFeaturePromise);
|
||||
|
||||
const component = await fixture(
|
||||
html`<chromedash-guide-stage-page></chromedash-guide-stage-page>`);
|
||||
assert.exists(component);
|
||||
assert.instanceOf(component, ChromedashGuideStagePage);
|
||||
|
||||
// invalid feature requests would trigger the toast to show message
|
||||
const toastEl = document.querySelector('chromedash-toast');
|
||||
const toastMsgSpan = toastEl.shadowRoot.querySelector('span#msg');
|
||||
assert.include(toastMsgSpan.innerHTML,
|
||||
'Some errors occurred. Please refresh the page or try again later.');
|
||||
});
|
||||
|
||||
it('renders with fake data (with implStatusForm and implStatusName)', async () => {
|
||||
const stageId = 1;
|
||||
const featureId = 123456;
|
||||
window.csClient.getFeature.withArgs(featureId).returns(validFeaturePromise);
|
||||
|
||||
const component = await fixture(
|
||||
html`<chromedash-guide-stage-page
|
||||
.stageId=${stageId}
|
||||
.featureId=${featureId}
|
||||
.featureForm=${featureForm}
|
||||
.implStatusForm=${implStatusForm}
|
||||
.implStatusName=${implStatusName}>
|
||||
</chromedash-guide-stage-page>`);
|
||||
assert.exists(component);
|
||||
assert.instanceOf(component, ChromedashGuideStagePage);
|
||||
|
||||
const subheaderDiv = component.shadowRoot.querySelector('div#subheader');
|
||||
assert.exists(subheaderDiv);
|
||||
// subheader title is correct and clickable
|
||||
assert.include(subheaderDiv.innerHTML, 'href="/guide/edit/123456"');
|
||||
assert.include(subheaderDiv.innerHTML, 'Edit feature:');
|
||||
|
||||
// feature form, hidden token field, and submit/cancel buttons exist
|
||||
const form = component.shadowRoot.querySelector('form[name="feature_form"]');
|
||||
assert.exists(form);
|
||||
assert.include(form.innerHTML, '<input type="hidden" name="token">');
|
||||
assert.include(form.innerHTML,
|
||||
'<input type="hidden" name="form_fields" value="fake feature field 1,'+
|
||||
'fake feature field 2,fake implStatus field 1,fake implStatus field 2">');
|
||||
assert.include(form.innerHTML, '<div class="final_buttons">');
|
||||
|
||||
// Implementation section renders correct title and fields
|
||||
assert.include(form.innerHTML, 'Implementation in Chromium');
|
||||
assert.include(form.innerHTML, 'fake implStatusName');
|
||||
assert.include(form.innerHTML, 'type="hidden" name="impl_status_offered"');
|
||||
assert.include(form.innerHTML, 'type="checkbox" name="set_impl_status"');
|
||||
assert.notInclude(form.innerHTML, 'This feature already has implementation status');
|
||||
});
|
||||
|
||||
it('renders with fake data (without implStatusForm and implStatusName)', async () => {
|
||||
const stageId = 1;
|
||||
const featureId = 123456;
|
||||
window.csClient.getFeature.withArgs(featureId).returns(validFeaturePromise);
|
||||
|
||||
const component = await fixture(
|
||||
html`<chromedash-guide-stage-page
|
||||
.stageId=${stageId}
|
||||
.featureId=${featureId}
|
||||
.featureForm=${featureForm}>
|
||||
</chromedash-guide-stage-page>`);
|
||||
assert.exists(component);
|
||||
assert.instanceOf(component, ChromedashGuideStagePage);
|
||||
|
||||
const form = component.shadowRoot.querySelector('form[name="feature_form"]');
|
||||
assert.exists(form);
|
||||
|
||||
// Implementation section renders correct title and fields
|
||||
assert.notInclude(form.innerHTML, 'Implementation in Chromium');
|
||||
assert.notInclude(form.innerHTML, 'This feature already has implementation status');
|
||||
assert.notInclude(form.innerHTML, 'fake implStatusName');
|
||||
assert.notInclude(form.innerHTML, 'type="hidden" name="impl_status_offered"');
|
||||
assert.notInclude(form.innerHTML, 'type="checkbox" name="set_impl_status"');
|
||||
});
|
||||
});
|
|
@ -13,19 +13,6 @@ export class ChromedashGuideVerifyAccuracyPage extends LitElement {
|
|||
...SHARED_STYLES,
|
||||
...FORM_STYLES,
|
||||
css`
|
||||
sl-skeleton {
|
||||
margin-bottom: 1em;
|
||||
width: 60%;
|
||||
}
|
||||
sl-skeleton:nth-of-type(even) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
h3 sl-skeleton {
|
||||
margin-top: 1em;
|
||||
width: 30%;
|
||||
height: 1.25em;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
const fields = document.querySelectorAll('input, textarea');
|
||||
for (let i = 0; i < fields.length; ++i) {
|
||||
fields[i].addEventListener('input', (e) => {
|
||||
e.target.classList.add('interacted');
|
||||
});
|
||||
}
|
||||
|
||||
// Allow editing if there was already a value specified in this
|
||||
// deprecated field.
|
||||
const timelineField = document.querySelector('#id_experiment_timeline');
|
||||
if (timelineField && timelineField.value) {
|
||||
timelineField.disabled = '';
|
||||
}
|
||||
|
||||
|
||||
if (document.querySelector('#cancel-button')) {
|
||||
document.querySelector('#cancel-button').addEventListener('click', (e) => {
|
||||
window.location.href = `/guide/edit/${e.currentTarget.dataset.id}`;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.classList.remove('loading');
|
||||
});
|
||||
|
||||
// Copy field SRC to DST if SRC is edited and DST was empty and
|
||||
// has not been edited.
|
||||
const COPY_ON_EDIT = [
|
||||
['dt_milestone_desktop_start', 'dt_milestone_android_start'],
|
||||
['dt_milestone_desktop_start', 'dt_milestone_webview_start'],
|
||||
// Don't autofill dt_milestone_ios_start because it is rare.
|
||||
['ot_milestone_desktop_start', 'ot_milestone_android_start'],
|
||||
['ot_milestone_desktop_end', 'ot_milestone_android_end'],
|
||||
['ot_milestone_desktop_start', 'ot_milestone_webview_start'],
|
||||
['ot_milestone_desktop_end', 'ot_milestone_webview_end'],
|
||||
];
|
||||
|
||||
for (let [srcId, dstId] of COPY_ON_EDIT) {
|
||||
let srcEl = document.getElementById('id_' + srcId);
|
||||
let dstEl = document.getElementById('id_' + dstId);
|
||||
if (srcEl && dstEl && srcEl.value == dstEl.value) {
|
||||
srcEl.addEventListener('input', (e) => {
|
||||
if (!dstEl.classList.contains('interacted')) {
|
||||
dstEl.value = srcEl.value;
|
||||
dstEl.classList.add('copied');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -212,4 +212,16 @@ export const FORM_STYLES = [
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
sl-skeleton {
|
||||
margin-bottom: 1em;
|
||||
width: 60%;
|
||||
}
|
||||
sl-skeleton:nth-of-type(even) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
h3 sl-skeleton {
|
||||
width: 30%;
|
||||
height: 1.5em;
|
||||
}
|
||||
`];
|
||||
|
|
|
@ -201,3 +201,16 @@ chromedash-form-field {
|
|||
}
|
||||
}
|
||||
|
||||
sl-skeleton {
|
||||
margin-bottom: 1em;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
sl-skeleton:nth-of-type(even) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
h3 sl-skeleton {
|
||||
width: 30%;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
|
|
@ -1,99 +1,13 @@
|
|||
{% extends "_base.html" %}
|
||||
{% block page_title %}{{ feature.name }} - {% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/forms.css?v={{app_version}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block subheader %}
|
||||
<div id="subheader">
|
||||
<h2 id="breadcrumbs">
|
||||
<a href="/guide/edit/{{ feature_id }}">
|
||||
<iron-icon icon="chromestatus:arrow-back"></iron-icon>
|
||||
Edit feature: {{ feature.name }}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
<h3>{{ stage_name }}</h3>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form name="feature_form" method="POST" action="{{current_path}}">
|
||||
<input type="hidden" name="token" value="{{xsrf_token}}">
|
||||
<input type="hidden" name="form_fields"
|
||||
value="{{ feature_form.fields.keys | join:',' }},
|
||||
{% if impl_status_form %}
|
||||
{{ impl_status_form.fields.keys | join:',' }}
|
||||
{% endif %}" >
|
||||
|
||||
<section class="stage_form">
|
||||
<chromedash-form-table>
|
||||
{{ feature_form }}
|
||||
|
||||
<chromedash-form-field name="set_stage"
|
||||
stage="{{ stage_name }}"
|
||||
value="{{ already_on_this_stage }}"
|
||||
{% if already_on_this_stage %}
|
||||
disabled
|
||||
{% endif %}
|
||||
>
|
||||
</chromedash-form-field>
|
||||
|
||||
</chromedash-form-table>
|
||||
</section>
|
||||
|
||||
|
||||
{% if impl_status_name or impl_status_form %}
|
||||
<h3>Implementation in Chromium</h3>
|
||||
<section class="stage_form">
|
||||
<chromedash-form-table>
|
||||
{% if impl_status_name %}
|
||||
<chromedash-form-field>
|
||||
<span slot="label">Implementation status:</span>
|
||||
|
||||
{% if already_on_this_impl_status %}
|
||||
<span slot="help">
|
||||
This feature already has implementation status:
|
||||
<b>{{ impl_status_name }}</b>.
|
||||
</td>
|
||||
</span>
|
||||
{% else %}
|
||||
<span slot="field">
|
||||
<input type="hidden" name="impl_status_offered"
|
||||
value="{{impl_status_offered}}">
|
||||
<input type="checkbox" name="set_impl_status"
|
||||
id="set_impl_status">
|
||||
<!-- TODO(jrobbins): When checked, make some milestone fields required. -->
|
||||
<label for="set_impl_status">
|
||||
Set implementation status to: <b>{{ impl_status_name }}</b>
|
||||
</label>
|
||||
</span>
|
||||
<span slot="help">
|
||||
Check this box to update the implementation
|
||||
status of this feature in Chromium.
|
||||
</span>
|
||||
{% endif %}
|
||||
</chromedash-form-field>
|
||||
{% endif %}
|
||||
|
||||
{{ impl_status_form }}
|
||||
|
||||
</chromedash-form-table>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div style="padding-left: 220px" class="final_buttons">
|
||||
<input class="button" type="submit" value="Submit">
|
||||
<button id="cancel-button" data-id="{{ feature_id }}"
|
||||
type="reset">Cancel</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="/static/js/admin/feature_form.min.js?v={{app_version}}"
|
||||
nonce="{{nonce}}"></script>
|
||||
<chromedash-guide-stage-page
|
||||
stageId="{{ stage_id }}"
|
||||
featureId="{{ feature_id }}"
|
||||
featureForm="{{ feature_form }}"
|
||||
implStatusForm="{{ impl_status_form }}"
|
||||
implStatusName="{{ impl_status_name }}"
|
||||
implStatusOffered="{{ impl_status_offered }}">
|
||||
</chromedash-guide-stage-page>
|
||||
{% endblock %}
|
||||
|
|
Загрузка…
Ссылка в новой задаче