Edit metadata from the feature detail page (#2619)
* metadata field can be edited from feature-detail * remove unused variables
This commit is contained in:
Родитель
21cf744501
Коммит
742a640a31
|
@ -31,10 +31,10 @@ setBasePath('/static/shoelace');
|
|||
// Configure shoelace to also find material design 24pt outline icons
|
||||
// like: <sl-icon-button library="material" name="unfold-more">
|
||||
// See developer-documentation.md for instructions adding icons.
|
||||
import { registerIconLibrary } from '@shoelace-style/shoelace/dist/utilities/icon-library.js';
|
||||
import {registerIconLibrary} from '@shoelace-style/shoelace/dist/utilities/icon-library.js';
|
||||
registerIconLibrary('material', {
|
||||
resolver: name => `/static/shoelace/assets/material-icons/${name}.svg`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor')
|
||||
resolver: name => `/static/shoelace/assets/material-icons/${name}.svg`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor'),
|
||||
});
|
||||
|
||||
|
||||
|
@ -65,6 +65,7 @@ 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-metadata-page';
|
||||
import './elements/chromedash-guide-verify-accuracy-page';
|
||||
import './elements/chromedash-header';
|
||||
import './elements/chromedash-legend';
|
||||
|
|
|
@ -215,6 +215,13 @@ class ChromedashApp extends LitElement {
|
|||
this.pageComponent.appTitle = this.appTitle;
|
||||
this.hideSidebar();
|
||||
});
|
||||
page('/guide/stage/:featureId(\\d+)/metadata', (ctx) => {
|
||||
this.pageComponent = document.createElement('chromedash-guide-metadata-page');
|
||||
this.pageComponent.featureId = parseInt(ctx.params.featureId);
|
||||
this.pageComponent.nextPage = this.currentPage;
|
||||
this.pageComponent.appTitle = this.appTitle;
|
||||
this.hideSidebar();
|
||||
});
|
||||
page('/settings', (ctx) => {
|
||||
this.pageComponent = document.createElement('chromedash-settings-page');
|
||||
this.currentPage = ctx.path;
|
||||
|
|
|
@ -308,7 +308,16 @@ class ChromedashFeatureDetail extends LitElement {
|
|||
if (fields === undefined || fields.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const editButton = html`
|
||||
<sl-button size="small" style="float:right"
|
||||
href="/guide/stage/${this.feature.id}/metadata"
|
||||
>Edit fields</sl-button>
|
||||
`;
|
||||
|
||||
const content = html`
|
||||
<p class="description">
|
||||
${this.canEdit ? editButton : nothing}
|
||||
</p>
|
||||
<section class="card">
|
||||
${this.renderSectionFields(fields, {})}
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import {LitElement, css, html} from 'lit';
|
||||
import {ref} from 'lit/directives/ref.js';
|
||||
import {showToastMessage} from './utils.js';
|
||||
import './chromedash-form-table';
|
||||
import './chromedash-form-field';
|
||||
import {FLAT_METADATA_FIELDS, formatFeatureForEdit} from './form-definition';
|
||||
import {ALL_FIELDS} from './form-field-specs';
|
||||
import {SHARED_STYLES} from '../sass/shared-css.js';
|
||||
import {FORM_STYLES} from '../sass/forms-css.js';
|
||||
|
||||
|
||||
export class ChromedashGuideMetadataPage extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
...FORM_STYLES,
|
||||
css`
|
||||
`];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
featureId: {type: Number},
|
||||
feature: {type: Object},
|
||||
loading: {type: Boolean},
|
||||
appTitle: {type: String},
|
||||
nextPage: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.featureId = 0;
|
||||
this.feature = {};
|
||||
this.loading = true;
|
||||
this.appTitle = '';
|
||||
this.nextPage = '';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
Promise.resolve(window.csClient.getFeature(this.featureId))
|
||||
.then((feature) => {
|
||||
this.feature = feature;
|
||||
if (this.feature.name) {
|
||||
document.title = `${this.feature.name} - ${this.appTitle}`;
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.title = this.appTitle;
|
||||
}
|
||||
|
||||
async registerHandlers(el) {
|
||||
if (!el) return;
|
||||
|
||||
/* Add the form's event listener after Shoelace event listeners are attached
|
||||
* see more at https://github.com/GoogleChrome/chromium-dashboard/issues/2014 */
|
||||
await el.updateComplete;
|
||||
const hiddenTokenField = this.shadowRoot.querySelector('input[name=token]');
|
||||
hiddenTokenField.form.addEventListener('submit', (event) => {
|
||||
this.handleFormSubmit(event, hiddenTokenField);
|
||||
});
|
||||
|
||||
this.addMiscEventListeners();
|
||||
this.scrollToPosition();
|
||||
}
|
||||
|
||||
handleFormSubmit(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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scrollToPosition() {
|
||||
if (location.hash) {
|
||||
const hash = decodeURIComponent(location.hash);
|
||||
if (hash) {
|
||||
const el = this.shadowRoot.querySelector(hash);
|
||||
el.scrollIntoView(true, {behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleCancelClick() {
|
||||
window.location.href = `/guide/edit/${this.featureId}`;
|
||||
}
|
||||
|
||||
// get a comma-spearated list of field names
|
||||
getFormFields() {
|
||||
const fields = FLAT_METADATA_FIELDS.sections.reduce(
|
||||
(combined, section) => [...combined, ...section.fields], []);
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
getNextPage() {
|
||||
return this.nextPage || `/guide/edit/${this.featureId}`;
|
||||
}
|
||||
|
||||
renderSubheader() {
|
||||
return html`
|
||||
<div id="subheader">
|
||||
<h2 id="breadcrumbs">
|
||||
<a href=${this.getNextPage()}>
|
||||
<iron-icon icon="chromestatus:arrow-back"></iron-icon>
|
||||
Edit feature: ${this.feature.name}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderFields(formattedFeature, section) {
|
||||
return section.fields.map(field => {
|
||||
const featureJSONKey = ALL_FIELDS[field].name || field;
|
||||
return html`
|
||||
<chromedash-form-field
|
||||
name=${field}
|
||||
value=${formattedFeature[featureJSONKey]}>
|
||||
</chromedash-form-field>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
renderSections(formattedFeature, sections) {
|
||||
return sections.map(section => {
|
||||
return html`
|
||||
<h3>${section.name}</h3>
|
||||
<section class="stage_form">
|
||||
${this.renderFields(formattedFeature, section)}
|
||||
</section>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const formattedFeature = formatFeatureForEdit(this.feature);
|
||||
return html`
|
||||
<form name="feature_form" method="POST"
|
||||
action="/guide/edit/${this.featureId}">
|
||||
<input type="hidden" name="token">
|
||||
<input type="hidden" name="form_fields" value=${this.getFormFields()} >
|
||||
<input type="hidden" name="nextPage" value=${this.getNextPage()} >
|
||||
|
||||
<chromedash-form-table ${ref(this.registerHandlers)}>
|
||||
${this.renderSections(formattedFeature, FLAT_METADATA_FIELDS.sections)}
|
||||
</chromedash-form-table>
|
||||
|
||||
<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-metadata-page', ChromedashGuideMetadataPage);
|
|
@ -1214,6 +1214,7 @@ function makeDisplaySpecs(fields) {
|
|||
export const DISPLAY_FIELDS_IN_STAGES = {
|
||||
'Metadata': makeDisplaySpecs([
|
||||
'category', 'feature_type', 'accurate_as_of', 'breaking_change',
|
||||
'search_tags', 'bug_url', 'launch_bug_url',
|
||||
]),
|
||||
[INTENT_STAGES.INTENT_INCUBATE[0]]: makeDisplaySpecs([
|
||||
'initial_public_proposal_url', 'explainer_links',
|
||||
|
|
2
main.py
2
main.py
|
@ -164,6 +164,8 @@ spa_page_routes = [
|
|||
defaults={'require_edit_feature': True}),
|
||||
Route('/guide/verify_accuracy/<int:feature_id>',
|
||||
defaults={'require_edit_feature': True}),
|
||||
Route('/guide/stage/<int:feature_id>/metadata',
|
||||
defaults={'require_edit_feature': True}),
|
||||
Route('/metrics'),
|
||||
Route('/metrics/css'),
|
||||
Route('/metrics/css/popularity'),
|
||||
|
|
Загрузка…
Ссылка в новой задаче