blob: df1b9927ae33f88c1d48881d555eb16092e9e42e [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_components/managed_footnote/managed_footnote.js';
import './item.js';
import './mv2_deprecation_panel.js';
import './review_panel.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {DummyItemDelegate} from './item.js';
import type {ExtensionsItemElement, ItemDelegate} from './item.js';
import {getCss} from './item_list.css.js';
import {getHtml} from './item_list.html.js';
import {getMv2ExperimentStage, Mv2ExperimentStage} from './mv2_deprecation_util.js';
type Filter = (info: chrome.developerPrivate.ExtensionInfo) => boolean;
const ExtensionsItemListElementBase = I18nMixinLit(CrLitElement);
export class ExtensionsItemListElement extends ExtensionsItemListElementBase {
static get is() {
return 'extensions-item-list';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
apps: {type: Array},
extensions: {type: Array},
delegate: {type: Object},
inDevMode: {
type: Boolean,
reflect: true,
},
isMv2DeprecationNoticeDismissed: {
type: Boolean,
notify: true,
reflect: true,
},
filter: {
type: String,
},
computedFilter_: {type: String},
maxColumns_: {type: Number},
filteredExtensions_: {type: Array},
filteredApps_: {type: Array},
/**
* List of potentially unsafe extensions that should be visible in the
* review panel.
*/
unsafeExtensions_: {type: Array},
/**
* Current Manifest V2 experiment stage.
*/
mv2ExperimentStage_: {type: Number},
/**
* List of extensions that are affected by the mv2 deprecation and should
* be visible in the mv2 deprecation panel.
*/
mv2DeprecatedExtensions_: {type: Array},
shownAppsCount_: {type: Number},
shownExtensionsCount_: {type: Number},
/**
* Indicates whether the review panel is shown.
*/
showSafetyCheckReviewPanel_: {type: Boolean},
/**
* Indicates if the review panel has ever been shown.
*/
reviewPanelShown_: {
type: Boolean,
state: true,
},
};
}
accessor apps: chrome.developerPrivate.ExtensionInfo[] = [];
accessor extensions: chrome.developerPrivate.ExtensionInfo[] = [];
accessor delegate: ItemDelegate = new DummyItemDelegate();
accessor inDevMode: boolean = false;
accessor isMv2DeprecationNoticeDismissed: boolean = false;
accessor filter: string = '';
protected accessor filteredExtensions_:
chrome.developerPrivate.ExtensionInfo[] = [];
protected accessor filteredApps_: chrome.developerPrivate.ExtensionInfo[] =
[];
protected accessor computedFilter_: Filter|null = null;
protected accessor maxColumns_: number = 3;
protected accessor unsafeExtensions_:
chrome.developerPrivate.ExtensionInfo[] = [];
protected accessor mv2ExperimentStage_: Mv2ExperimentStage =
getMv2ExperimentStage(loadTimeData.getInteger('MV2ExperimentStage'));
protected accessor mv2DeprecatedExtensions_:
chrome.developerPrivate.ExtensionInfo[] = [];
protected accessor shownAppsCount_: number = 0;
protected accessor shownExtensionsCount_: number = 0;
protected accessor showSafetyCheckReviewPanel_: boolean = false;
private accessor reviewPanelShown_: boolean = false;
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has('filter')) {
this.computedFilter_ = this.computeFilter_();
}
if (changedProperties.has('filter') ||
changedProperties.has('extensions')) {
this.filteredExtensions_ = this.computedFilter_ ?
this.extensions.filter(
extension => this.computedFilter_!(extension)) :
this.extensions;
this.shownExtensionsCount_ = this.filteredExtensions_.length;
}
if (changedProperties.has('filter') || changedProperties.has('apps')) {
this.filteredApps_ = this.computedFilter_ ?
this.apps.filter(app => this.computedFilter_!(app)) :
this.apps;
this.shownAppsCount_ = this.filteredApps_.length;
}
if (changedProperties.has('extensions')) {
this.unsafeExtensions_ = this.computeUnsafeExtensions_();
this.showSafetyCheckReviewPanel_ =
this.computeShowSafetyCheckReviewPanel_();
this.mv2DeprecatedExtensions_ = this.computeMv2DeprecatedExtensions_();
}
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('computedFilter_')) {
this.announceSearchResults_();
}
}
getDetailsButton(id: string): HTMLElement|null {
const item = this.shadowRoot.querySelector<ExtensionsItemElement>(`#${id}`);
return item && item.getDetailsButton();
}
getRemoveButton(id: string): HTMLElement|null {
const item = this.shadowRoot.querySelector<ExtensionsItemElement>(`#${id}`);
return item && item.getRemoveButton();
}
getErrorsButton(id: string): HTMLElement|null {
const item = this.shadowRoot.querySelector<ExtensionsItemElement>(`#${id}`);
return item && item.getErrorsButton();
}
/**
* Focus the remove button for the item matching `id`. If the remove button is
* not visible, focus the details button instead.
* return: If an item's button has been focused, see comment below.
*/
focusItemButton(id: string): boolean {
const item = this.shadowRoot.querySelector<ExtensionsItemElement>(`#${id}`);
// This function is called from a setTimeout() inside manager.ts. Rarely,
// the list of extensions rendered in this element may not match the list of
// extensions stored in manager.ts for a brief moment (not visible to the
// user). As a result, `item` here may be null even though `id` points to
// an extension inside `manager.ts`. If this happens, do not focus anything.
// Observed in crbug.com/1482580.
if (!item) {
return false;
}
const buttonToFocus = item.getRemoveButton() || item.getDetailsButton();
buttonToFocus.focus();
return true;
}
/**
* Computes the filter function to be used for determining which items
* should be shown. A |null| value indicates that everything should be
* shown.
*/
private computeFilter_(): Filter|null {
const formattedFilter = this.filter.trim().toLowerCase();
if (!formattedFilter) {
return null;
}
return i => [i.name, i.id].some(
s => s.toLowerCase().includes(formattedFilter));
}
/**
* Computes the extensions that are affected by the manifest v2 deprecation
* and should be visible in the MV2 deprecation panel.
*/
private computeMv2DeprecatedExtensions_():
chrome.developerPrivate.ExtensionInfo[] {
return this.extensions.filter((extension) => {
switch (this.mv2ExperimentStage_) {
case Mv2ExperimentStage.NONE:
return false;
case Mv2ExperimentStage.WARNING:
return extension.isAffectedByMV2Deprecation &&
!extension.didAcknowledgeMV2DeprecationNotice;
case Mv2ExperimentStage.DISABLE_WITH_REENABLE:
return extension.isAffectedByMV2Deprecation &&
extension.disableReasons.unsupportedManifestVersion &&
!extension.didAcknowledgeMV2DeprecationNotice;
case Mv2ExperimentStage.UNSUPPORTED:
return extension.isAffectedByMV2Deprecation &&
extension.disableReasons.unsupportedManifestVersion;
}
});
}
/**
* Computes the extensions that are potentially unsafe and should be visible
* in the review panel.
*/
private computeUnsafeExtensions_(): chrome.developerPrivate.ExtensionInfo[] {
return this.extensions.filter(
extension =>
!!(extension.safetyCheckText &&
extension.safetyCheckText.panelString));
}
/**
* Returns whether the review deprecation panel should be visible.
*/
private computeShowSafetyCheckReviewPanel_(): boolean {
// If there are any unsafe extensions, they will be shown in the panel.
// Store this, so we can show the completion info in the panel when there
// are no unsafe extensions left after the user finished reviewing the
// extensions.
if (this.unsafeExtensions_.length !== 0) {
this.reviewPanelShown_ = true;
}
// Panel is visible if there are any unsafe extensions, or the there are
// none left after the user finished reviewing the extensions.
return this.unsafeExtensions_.length !== 0 || this.reviewPanelShown_;
}
/*
* Indicates whether the mv2 deprecation panel is shown.
*/
protected hasSafetyCheckTriggeringExtension_(): boolean {
for (const extension of this.extensions) {
if (!!extension.safetyCheckText &&
!!extension.safetyCheckText.panelString &&
this.showSafetyCheckReviewPanel_) {
return true;
}
}
return false;
}
/**
* Returns whether the manifest v2 deprecation panel should be visible.
*/
protected shouldShowMv2DeprecationPanel_(): boolean {
switch (this.mv2ExperimentStage_) {
case Mv2ExperimentStage.NONE:
return false;
case Mv2ExperimentStage.WARNING:
case Mv2ExperimentStage.DISABLE_WITH_REENABLE:
case Mv2ExperimentStage.UNSUPPORTED:
// Panel is visible when it has not been dismissed and at least one
// extension is affected by the MV2 deprecation.
return !this.isMv2DeprecationNoticeDismissed &&
this.mv2DeprecatedExtensions_?.length !== 0;
}
}
protected shouldShowEmptyItemsMessage_(): boolean {
return this.apps.length === 0 && this.extensions.length === 0;
}
protected shouldShowEmptySearchMessage_(): boolean {
return !this.shouldShowEmptyItemsMessage_() && this.shownAppsCount_ === 0 &&
this.shownExtensionsCount_ === 0;
}
protected onNoExtensionsClick_(e: Event) {
if ((e.target as HTMLElement).tagName === 'A') {
chrome.metricsPrivate.recordUserAction('Options_GetMoreExtensions');
}
}
private announceSearchResults_() {
if (this.computedFilter_) {
setTimeout(() => { // Async to allow list to update.
const total = this.shownAppsCount_ + this.shownExtensionsCount_;
getAnnouncerInstance().announce(this.shouldShowEmptySearchMessage_() ?
this.i18n('noSearchResults') :
(total === 1 ?
this.i18n('searchResultsSingular', this.filter) :
this.i18n(
'searchResultsPlural', total.toString(), this.filter)));
}, 0);
}
}
}
declare global {
interface HTMLElementTagNameMap {
'extensions-item-list': ExtensionsItemListElement;
}
}
customElements.define(ExtensionsItemListElement.is, ExtensionsItemListElement);