diff --git a/api/api_specs.py b/api/api_specs.py index 133a90ec9573..18fe5078639c 100644 --- a/api/api_specs.py +++ b/api/api_specs.py @@ -78,7 +78,7 @@ ('search_tags', 'split_str'), ('security_review_status', 'int'), ('security_risks', 'str'), - ('spec_link', 'link'), + ('spec_links', 'links'), ('spec_mentor_emails', 'emails'), ('standard_maturity', 'int'), ('summary', 'str'), diff --git a/api/converters.py b/api/converters.py index 963f707b4d1d..2af9a4793a9c 100644 --- a/api/converters.py +++ b/api/converters.py @@ -400,7 +400,7 @@ def feature_entry_to_json_verbose( 'initial_public_proposal_url': fe.initial_public_proposal_url, 'explainer_links': fe.explainer_links, 'requires_embedder_support': fe.requires_embedder_support, - 'spec_link': fe.spec_link, + 'spec_links': fe.spec_links, 'api_spec': fe.api_spec, 'interop_compat_risks': fe.interop_compat_risks, 'all_platforms': fe.all_platforms, @@ -496,7 +496,7 @@ def feature_entry_to_json_verbose( 'enterprise_feature_categories': fe.enterprise_feature_categories or [], 'enterprise_product_category': fe.enterprise_product_category or ENTERPRISE_PRODUCT_CATEGORY_CHROME_BROWSER_UPDATE, 'standards': { - 'spec': fe.spec_link, + 'specs': fe.spec_links, 'maturity': { 'text': STANDARD_MATURITY_CHOICES.get(fe.standard_maturity), 'short_text': STANDARD_MATURITY_SHORT.get(fe.standard_maturity), @@ -552,7 +552,7 @@ def feature_entry_to_json_basic(fe: FeatureEntry, 'updated': {'by': fe.updater_email, 'when': _date_to_str(fe.updated)}, 'accurate_as_of': _date_to_str(fe.accurate_as_of), 'standards': { - 'spec': fe.spec_link, + 'specs': fe.spec_links, 'maturity': { 'text': STANDARD_MATURITY_CHOICES.get(fe.standard_maturity), 'short_text': STANDARD_MATURITY_SHORT.get(fe.standard_maturity), diff --git a/api/converters_test.py b/api/converters_test.py index 0e922c94fed0..011acda65e3e 100644 --- a/api/converters_test.py +++ b/api/converters_test.py @@ -49,7 +49,7 @@ def setUp(self): owner_emails=['feature_owner@example.com'], feature_type=0, editor_emails=['feature_editor@example.com', 'owner_1@example.com'], impl_status_chrome=5, blink_components=['Blink'], shipping_year=2024, - spec_link='https://example.com/spec', + spec_links=['https://example.com/spec'], sample_links=['https://example.com/samples'], screenshot_links=['https://example.com/screenshot'], first_enterprise_notification_milestone=100, standard_maturity=1, @@ -325,7 +325,7 @@ def test_feature_entry_to_json_verbose__normal(self): 'doc_links': ['https://example.com/docs'], 'prefixed': False, 'requires_embedder_support': False, - 'spec_link': 'https://example.com/spec', + 'spec_links': ['https://example.com/spec'], 'sample_links': ['https://example.com/samples'], 'screenshot_links': ['https://example.com/screenshot'], 'first_enterprise_notification_milestone': 100, diff --git a/api/legacy_converters.py b/api/legacy_converters.py index bca33d920f25..41056dc6d415 100644 --- a/api/legacy_converters.py +++ b/api/legacy_converters.py @@ -51,7 +51,7 @@ def feature_to_legacy_json(f: Feature) -> dict[str, Any]: } d['accurate_as_of'] = d.pop('accurate_as_of', None) d['standards'] = { - 'spec': d.pop('spec_link', None), + 'specs': d.pop('spec_links', None), 'status': { 'text': STANDARDIZATION[f.standardization], 'val': d.pop('standardization', None), diff --git a/api/processes_api_test.py b/api/processes_api_test.py index dcd4a4500b02..d7974fdf72b4 100644 --- a/api/processes_api_test.py +++ b/api/processes_api_test.py @@ -165,7 +165,7 @@ def setUp(self): self.feature_1 = core_models.FeatureEntry( name='feature one', summary='sum Z', owner_emails=['feature_owner@example.com'], - spec_link='fake spec link', category=1, web_dev_views=1, + spec_links=['fake spec link'], category=1, web_dev_views=1, impl_status_chrome=5, intent_stage=core_enums.INTENT_IMPLEMENT, feature_type=0) self.feature_1.put() diff --git a/client-src/elements/chromedash-feature.ts b/client-src/elements/chromedash-feature.ts index a74fffb61200..7f4389f56bd1 100644 --- a/client-src/elements/chromedash-feature.ts +++ b/client-src/elements/chromedash-feature.ts @@ -56,6 +56,23 @@ class ChromedashFeature extends LitElement { super.update(changedProperties); } + updated(changedProps: Map) { + if ( + (changedProps.has('open') || changedProps.has('feature')) && + this.open && + this.feature?.standards?.specs?.length > 1 + ) { + const dropdown = this.shadowRoot?.querySelector('sl-dropdown'); + const trigger = dropdown?.querySelector('[slot="trigger"]'); + if (dropdown && trigger && !trigger.hasAttribute('listeners-attached')) { + trigger.setAttribute('listeners-attached', 'true'); + + trigger.addEventListener('mouseenter', () => dropdown.show()); + dropdown.addEventListener('mouseleave', () => dropdown.hide()); + } + } + } + _initializeValues() { this._crBugNumber = this._getCrBugNumber(); this._newBugUrl = this._getNewBugUrl(); @@ -538,20 +555,43 @@ class ChromedashFeature extends LitElement { this.feature.standards.maturity.val}" .max="${MAX_STANDARDS_VAL}" > - ${this.feature.standards.spec + ${Array.isArray(this.feature.standards.specs) && + this.feature.standards.specs.length === 1 ? html` ${this.feature.standards.maturity.short_text} + ${this.feature.standards.maturity.short_text} + ` - : html` - - `} + : Array.isArray(this.feature.standards.specs) && + this.feature.standards.specs.length > 1 + ? html` + + + ${this.feature.standards.maturity.short_text} + + + + ${this.feature.standards.specs.map( + spec => + html` + ${spec} + ` + )} + + + ` + : html` + + `}
diff --git a/client-src/elements/form-definition.ts b/client-src/elements/form-definition.ts index fedd2087f993..3a004734eceb 100644 --- a/client-src/elements/form-definition.ts +++ b/client-src/elements/form-definition.ts @@ -9,7 +9,7 @@ export interface FormattedFeature { feature_type: number; intent_stage: number; accurate_as_of: boolean; - spec_link?: string; + spec_links?: string[]; standard_maturity: number; tag_review_status?: number; security_review_status?: number; @@ -92,7 +92,7 @@ export function formatFeatureForEdit(feature: Feature): FormattedFeature { accurate_as_of: true, // from feature.standards - spec_link: feature.standards.spec, + spec_links: feature.standards.specs, standard_maturity: feature.standards.maturity.val, tag_review_status: feature.tag_review_status_int, @@ -278,7 +278,7 @@ const FLAT_PROTOTYPE_FIELDS: MetadataFields = { name: 'Prototype a solution', fields: [ 'motivation', - 'spec_link', + 'spec_links', 'standard_maturity', 'api_spec', 'spec_mentors', @@ -469,7 +469,7 @@ const PSA_IMPLEMENT_FIELDS: MetadataFields = { // Standardization { name: 'Start prototyping', - fields: ['motivation', 'spec_link', 'standard_maturity'], + fields: ['motivation', 'spec_links', 'standard_maturity'], }, ], }; @@ -502,7 +502,7 @@ const DEPRECATION_PLAN_FIELDS: MetadataFields = { sections: [ { name: 'Write up deprecation plan', - fields: ['motivation', 'spec_link'], + fields: ['motivation', 'spec_links'], }, ], }; diff --git a/client-src/elements/form-field-specs.ts b/client-src/elements/form-field-specs.ts index 4e5dde0ebff7..40281192a5e0 100644 --- a/client-src/elements/form-field-specs.ts +++ b/client-src/elements/form-field-specs.ts @@ -759,14 +759,14 @@ export const ALL_FIELDS: Record = { ), }, - spec_link: { - type: 'input', - attrs: URL_FIELD_ATTRS, + spec_links: { + type: 'textarea', + attrs: MULTI_URL_FIELD_ATTRS, required: false, - label: 'Spec link', - help_text: html` Link to the spec, if and when available. When implementing - a spec update, please link to a heading in a published spec rather than a - pull request when possible.`, + label: 'Spec link(s)', + help_text: html` Link to the spec(s) (one URL per line), if and when + available. When implementing a spec update, please link to a heading in a + published spec rather than a pull request when possible.`, extra_help: html`

Specifications should be written in the format and hosted in the URL space expected by your target standards body. For example, the W3C expects diff --git a/client-src/elements/queriable-fields.ts b/client-src/elements/queriable-fields.ts index df8ccf1a5e79..5fad6222f2e6 100644 --- a/client-src/elements/queriable-fields.ts +++ b/client-src/elements/queriable-fields.ts @@ -130,7 +130,7 @@ export const QUERIABLE_FIELDS: QueryField[] = [ }, // 'standards.maturity': Feature.standard_maturity, - // 'standards.spec': Feature.spec_link, + // 'standards.spec': Feature.spec_links, // 'api_spec': Feature.api_spec, // 'spec_mentors': Feature.spec_mentors, // 'security_review_status': Feature.security_review_status, diff --git a/client-src/elements/utils.ts b/client-src/elements/utils.ts index 40afe9dfe270..78f2ef0bd8fd 100644 --- a/client-src/elements/utils.ts +++ b/client-src/elements/utils.ts @@ -242,7 +242,7 @@ export function getFieldValueFromFeature( owner: 'browsers.chrome.owners', editors: 'editors', search_tags: 'tags', - spec_link: 'standards.spec', + spec_links: 'standards.specs', standard_maturity: 'standards.maturity.text', sample_links: 'resources.samples', docs_links: 'resources.docs', diff --git a/client-src/js-src/cs-client.js b/client-src/js-src/cs-client.js index cecfa9227b71..90628b0c8149 100644 --- a/client-src/js-src/cs-client.js +++ b/client-src/js-src/cs-client.js @@ -113,7 +113,7 @@ /** * @typedef {Object} FeatureDictInnerStandardsInfo - * @property {string} [spec] + * @property {string[]} specs * @property {FeatureDictInnerMaturityInfo} maturity */ @@ -230,7 +230,7 @@ * @property {string} [initial_public_proposal_url] * @property {string[]} explainer_links * @property {boolean} requires_embedder_support - * @property {string} [spec_link] + * @property {string} [spec_links] * @property {string} [api_spec] * @property {boolean} [prefixed] * @property {string} [interop_compat_risks] diff --git a/data/dev_data.json b/data/dev_data.json index 6a1a583c3103..d3caa449354f 100644 --- a/data/dev_data.json +++ b/data/dev_data.json @@ -50,7 +50,7 @@ ], "requires_embedder_support": false, "standard_maturity": 1, - "spec_link": "https://example.com/spec_link", + "spec_links": ["https://example.com/spec_link"], "api_spec": true, "spec_mentor_emails": [ "mentor_1@example.com", @@ -140,7 +140,7 @@ ], "requires_embedder_support": false, "standard_maturity": 1, - "spec_link": "https://example.com/spec_link", + "spec_links": ["https://example.com/spec_link"], "api_spec": true, "spec_mentor_emails": [ "mentor_1@example.com", @@ -232,7 +232,7 @@ ], "requires_embedder_support": true, "standard_maturity": 4, - "spec_link": "https://example.com/spec_link", + "spec_links": ["https://example.com/spec_link"], "api_spec": false, "spec_mentor_emails": [ "mentor_3@example.com", @@ -323,7 +323,7 @@ ], "requires_embedder_support": true, "standard_maturity": 4, - "spec_link": "https://example.com/spec_link", + "spec_links": ["https://example.com/spec_link"], "api_spec": false, "spec_mentor_emails": [ "mentor_3@example.com", diff --git a/internals/core_models.py b/internals/core_models.py index 91cb106bc983..63be1b3e82a5 100644 --- a/internals/core_models.py +++ b/internals/core_models.py @@ -131,7 +131,7 @@ class FeatureEntry(ndb.Model): # Copy from Feature explainer_links = ndb.StringProperty(repeated=True) requires_embedder_support = ndb.BooleanProperty(default=False) standard_maturity = ndb.IntegerProperty(required=True, default=UNSET_STD) - spec_link = ndb.StringProperty() + spec_links = ndb.StringProperty(repeated=True) api_spec = ndb.BooleanProperty(default=False) spec_mentor_emails = ndb.StringProperty(repeated=True) interop_compat_risks = ndb.TextProperty() diff --git a/internals/data_types.py b/internals/data_types.py index 2f620d78c832..6317e494c3c2 100644 --- a/internals/data_types.py +++ b/internals/data_types.py @@ -241,7 +241,7 @@ class VerboseFeatureDict(TypedDict): initial_public_proposal_url: str | None explainer_links: list[str] requires_embedder_support: bool - spec_link: str | None + spec_links: list[str] api_spec: str | None prefixed: bool | None interop_compat_risks: str | None diff --git a/internals/processes.py b/internals/processes.py index 68d25a408406..4586e36e244b 100644 --- a/internals/processes.py +++ b/internals/processes.py @@ -82,7 +82,8 @@ def process_to_dict(process): PI_MOTIVATION = ProgressItem('Motivation', 'motivation') PI_EXPLAINER = ProgressItem('Explainer', 'explainer_links') -PI_SPEC_LINK = ProgressItem('Spec link', 'spec_link') +# here plural? +PI_SPEC_LINK = ProgressItem('Spec links', 'spec_links') PI_SPEC_MENTOR = ProgressItem('Spec mentor', 'spec_mentors') PI_DRAFT_API_SPEC = ProgressItem('Draft API spec') PI_I2P_EMAIL = ProgressItem( @@ -678,11 +679,11 @@ def review_is_done(status): 'Doc links': lambda f, _: f.doc_links and f.doc_links[0], - 'Spec link': - lambda f, _: f.spec_link, + 'Spec links': + lambda f, _: f.spec_links, - 'Draft API spec': - lambda f, _: f.spec_link, + 'Draft API specs': + lambda f, _: f.spec_links, 'API spec': lambda f, _: f.api_spec, diff --git a/internals/search_fulltext.py b/internals/search_fulltext.py index 661d5ce35287..ef02dfda3974 100644 --- a/internals/search_fulltext.py +++ b/internals/search_fulltext.py @@ -79,7 +79,7 @@ def _get_strings_dict(fe: FeatureEntry) -> dict[str, list[str|None]]: 'initial_public_proposal_url': [fe.initial_public_proposal_url], 'explainer': fe.explainer_links, # TODO: standard_maturity - 'standards.spec': [fe.spec_link], + 'standards.spec': fe.spec_links, 'spec_mentors': fe.spec_mentor_emails, 'interop_compat_risks': [fe.interop_compat_risks], 'all_platforms_descr': [fe.all_platforms_descr], diff --git a/internals/search_queries.py b/internals/search_queries.py index 7d884169fd99..04120b764419 100644 --- a/internals/search_queries.py +++ b/internals/search_queries.py @@ -294,7 +294,7 @@ def sorted_by_review_date(descending: bool) -> Future: FeatureEntry.initial_public_proposal_url, 'explainer': FeatureEntry.explainer_links, 'requires_embedder_support': FeatureEntry.requires_embedder_support, - 'standards.spec': FeatureEntry.spec_link, + 'standards.specs': FeatureEntry.spec_links, 'api_spec': FeatureEntry.api_spec, 'spec_mentors': FeatureEntry.spec_mentor_emails, 'interop_compat_risks': FeatureEntry.interop_compat_risks, diff --git a/scripts/seed_datastore.py b/scripts/seed_datastore.py index f2190ba2cd83..a69c8fa70278 100755 --- a/scripts/seed_datastore.py +++ b/scripts/seed_datastore.py @@ -84,7 +84,7 @@ def add_features(server: str, after: datetime, detailsAfter: datetime): fe.devrel_emails = f['browsers']['chrome']['devrel'] fe.owner_emails = f['browsers']['chrome']['owners'] fe.prefixed = f['browsers']['chrome']['prefixed'] - fe.spec_link = f['standards']['spec'] + fe.spec_links = f['standards']['specs'] fe.standard_maturity = f['standards']['maturity']['val'] fe.ff_views = f['browsers']['ff']['view']['val'] fe.ff_views_link = f['browsers']['ff']['view']['url']