| /** |
| * @license |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| import {CodeOwnersModelMixin} from './code-owners-model-mixin'; |
| import {SuggestionsState, SuggestionsType} from './code-owners-model'; |
| import {getDisplayOwnersGroups, GroupedFiles} from './suggest-owners-util'; |
| import {customElement, property, state} from 'lit/decorators'; |
| import {css, html, LitElement, nothing, PropertyValues} from 'lit'; |
| import {when} from 'lit/directives/when'; |
| import {ifDefined} from 'lit/directives/if-defined'; |
| import {CodeOwnerInfo, CodeOwnersInfo, FetchedFile} from './code-owners-api'; |
| import { |
| AccountId, |
| AccountInfo, |
| GroupInfo, |
| } from '@gerritcodereview/typescript-api/rest-api'; |
| |
| const SUGGESTION_POLLING_INTERVAL = 1000; |
| |
| export const OWNER_GROUP_FILE_LIST = 'owner-group-file-list'; |
| |
| @customElement(OWNER_GROUP_FILE_LIST) |
| export class OwnerGroupFileList extends LitElement { |
| @property({type: Array}) |
| files?: Array<FetchedFile>; |
| |
| static override get styles() { |
| return [ |
| css` |
| :host { |
| display: block; |
| max-height: 500px; |
| overflow: auto; |
| } |
| span { |
| display: inline-block; |
| border-radius: var(--border-radius); |
| margin-left: var(--spacing-s); |
| padding: 0 var(--spacing-m); |
| color: var(--primary-text-color); |
| font-size: var(--font-size-small); |
| } |
| .Renamed { |
| background-color: var(--dark-remove-highlight-color); |
| margin: var(--spacing-s) 0; |
| } |
| `, |
| ]; |
| } |
| |
| override render() { |
| return html` |
| <ul> |
| ${this.files?.map(file => this.renderFile(file))} |
| </ul> |
| `; |
| } |
| |
| private renderFile(file: FetchedFile) { |
| return html` |
| <li> |
| ${this.computeFilePath(file)} |
| <strong>${this.computeFileName(file)}</strong> |
| ${when( |
| !!file.status, |
| () => html` |
| <span class=${ifDefined(file.status)}> ${file.status} </span> |
| ` |
| )} |
| </li> |
| `; |
| } |
| |
| computeFilePath(file: FetchedFile) { |
| const parts = file.path.split('/'); |
| return parts.slice(0, parts.length - 1).join('/') + '/'; |
| } |
| |
| computeFileName(file: FetchedFile) { |
| const parts = file.path.split('/'); |
| return parts.pop(); |
| } |
| } |
| |
| const base = CodeOwnersModelMixin(LitElement); |
| export const SUGGEST_OWNERS = 'suggest-owners'; |
| @customElement(SUGGEST_OWNERS) |
| export class SuggestOwners extends base { |
| private reportedEvents: Map< |
| SuggestionsType, |
| { |
| fetchingStart: boolean; |
| fetchingFinished: boolean; |
| } |
| >; |
| |
| // Bound by the extension point. |
| @property({type: Array}) |
| reviewers?: Array<AccountInfo | GroupInfo>; |
| |
| @state() |
| suggestedOwners?: Array<GroupedFiles>; |
| |
| @state() |
| showAllOwners = false; |
| |
| private progressUpdateTimer?: number; |
| |
| constructor() { |
| super(); |
| this.reportedEvents = new Map(); |
| for (const suggestionType of Object.values(SuggestionsType)) { |
| this.reportedEvents.set(suggestionType, { |
| fetchingStart: false, |
| fetchingFinished: false, |
| }); |
| } |
| } |
| |
| static override get styles() { |
| return [ |
| css` |
| :host[(show-suggestions)] { |
| display: block; |
| background-color: var(--view-background-color); |
| border: 1px solid var(--view-background-color); |
| border-radius: var(--border-radius); |
| box-shadow: var(--elevation-level-1); |
| padding: 0 var(--spacing-m); |
| margin: var(--spacing-m) 0; |
| } |
| .loadingSpin { |
| display: inline-block; |
| } |
| li { |
| list-style: none; |
| } |
| .suggestion-container { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| .flex-break { |
| height: 0; |
| flex-basis: 100%; |
| } |
| .suggestion-row, |
| .show-all-owners-row { |
| display: flex; |
| flex-direction: row; |
| align-items: flex-start; |
| } |
| .suggestion-row { |
| flex-wrap: wrap; |
| border-top: 1px solid var(--border-color); |
| padding: var(--spacing-s) 0; |
| } |
| .show-all-owners-row { |
| padding: var(--spacing-m) var(--spacing-xl) var(--spacing-s) 0; |
| } |
| .show-all-owners-row .loading { |
| padding: 0; |
| } |
| .show-all-owners-row .show-all-label { |
| margin-left: auto; /* align label to the right */ |
| } |
| .suggestion-row-indicator { |
| margin-right: var(--spacing-s); |
| visibility: hidden; |
| line-height: 26px; |
| } |
| .suggestion-row-indicator[visible] { |
| visibility: visible; |
| } |
| .suggestion-row-indicator[visible] iron-icon { |
| color: var(--link-color); |
| vertical-align: top; |
| position: relative; |
| --iron-icon-height: 18px; |
| --iron-icon-width: 18px; |
| top: 4px; /* (26-18)/2 - 26px line-height and 18px icon */ |
| } |
| .suggestion-group-name { |
| width: 260px; |
| line-height: 26px; |
| text-overflow: ellipsis; |
| overflow: hidden; |
| padding-right: var(--spacing-s); |
| white-space: nowrap; |
| } |
| .group-name-content { |
| display: flex; |
| align-items: center; |
| } |
| .group-name-content .group-name { |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| .group-name-prefix { |
| padding-left: var(--spacing-s); |
| white-space: nowrap; |
| color: var(--deemphasized-text-color); |
| } |
| .suggested-owners { |
| --account-gap: var(--spacing-xs); |
| --negative-account-gap: calc(-1 * var(--account-gap)); |
| margin: var(--negative-account-gap) 0 0 var(--negative-account-gap); |
| flex: 1; |
| } |
| .fetch-error-content, |
| .owned-by-all-users-content, |
| .no-owners-content { |
| line-height: 26px; |
| flex: 1; |
| padding-left: var(--spacing-m); |
| } |
| |
| .owned-by-all-users-content iron-icon { |
| width: 16px; |
| height: 16px; |
| padding-top: 5px; |
| } |
| |
| .fetch-error-content { |
| color: var(--error-text-color); |
| } |
| .no-owners-content a { |
| padding-left: var(--spacing-s); |
| } |
| .no-owners-content a iron-icon { |
| width: 16px; |
| height: 16px; |
| padding-top: 5px; |
| } |
| gr-account-label { |
| display: inline-block; |
| padding: var(--spacing-xs) var(--spacing-m); |
| user-select: none; |
| border: 1px solid transparent; |
| --label-border-radius: 8px; |
| /* account-max-length defines the max text width inside account-label. |
| With 59 the gr-account-label always has width <= 100px and 5 labels |
| are always fit in a single row */ |
| --account-max-length: 59px; |
| border: 1px solid var(--border-color); |
| margin: var(--account-gap) 0 0 var(--account-gap); |
| } |
| gr-account-label:focus { |
| outline: none; |
| } |
| gr-account-label:hover { |
| box-shadow: var(--elevation-level-1); |
| cursor: pointer; |
| } |
| gr-account-label[selected] { |
| background-color: var(--chip-selected-background-color); |
| border: 1px solid var(--chip-selected-background-color); |
| color: var(--chip-selected-text-color); |
| } |
| gr-hovercard { |
| max-width: 800px; |
| } |
| .loading { |
| display: flex; |
| align-content: center; |
| align-items: center; |
| justify-content: center; |
| padding: var(--spacing-m); |
| } |
| .loadingSpin { |
| width: 18px; |
| height: 18px; |
| margin-right: var(--spacing-m); |
| } |
| /* Copied from shared-styles */ |
| ul { |
| border: 0; |
| box-sizing: border-box; |
| font-size: 100%; |
| margin: 0; |
| padding: 0; |
| vertical-align: baseline; |
| } |
| *::after, |
| *::before { |
| box-sizing: border-box; |
| } |
| `, |
| ]; |
| } |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| this.stopUpdateProgressTimer(); |
| this.modelLoader?.pauseActiveSuggestedOwnersLoading(); |
| } |
| |
| protected override willUpdate(changedProperties: PropertyValues): void { |
| super.willUpdate(changedProperties); |
| if (changedProperties.has('showAllOwners')) { |
| this.showAllOwnersChanged(); |
| } |
| if (changedProperties.has('reviewers')) { |
| this.onReviewerChanged(); |
| } |
| if (changedProperties.has('showSuggestions')) { |
| this.onShowSuggestionsChanged(); |
| } |
| if ( |
| changedProperties.has('showSuggestions') || |
| changedProperties.has('selectedSuggestionsType') |
| ) { |
| this.onShowSuggestionsTypeChanged(); |
| } |
| if (changedProperties.has('selectedSuggestionsState')) { |
| this.onSuggestionsStateChanged(); |
| } |
| if ( |
| changedProperties.has('selectedSuggestionsFiles') || |
| changedProperties.has('selectedSuggestionsType') || |
| changedProperties.has('selectedSuggestionsState') || |
| changedProperties.has('reviewers') |
| ) { |
| this.onSuggestionsFilesChanged(); |
| } |
| } |
| |
| override render() { |
| if (!this.showSuggestions) return nothing; |
| return html` |
| <ul class="suggestion-container"> |
| <li class="show-all-owners-row"> |
| ${this.renderLoading()} ${this.renderShowAllOwnersCheckbox()} |
| </li> |
| </ul> |
| <ul class="suggestion-container"> |
| ${(this.suggestedOwners ?? []).map((s, i) => |
| this.renderSuggestion(s, i) |
| )} |
| </ul> |
| `; |
| } |
| |
| private renderLoading() { |
| const isLoading = |
| this.selectedSuggestionsState === SuggestionsState.Loading; |
| if (!isLoading) return nothing; |
| return html` |
| <p class="loading"> |
| <span class="loadingSpin"></span> |
| ${this.selectedSuggestionsLoadProgress} |
| </p> |
| `; |
| } |
| |
| private renderShowAllOwnersCheckbox() { |
| return html` |
| <label class="show-all-label"> |
| <input |
| id="showAllOwnersCheckbox" |
| type="checkbox" |
| .checked=${this.showAllOwners} |
| @click=${() => { |
| this.showAllOwners = !this.showAllOwners; |
| }} |
| /> |
| Show all owners |
| </label> |
| `; |
| } |
| |
| private renderSuggestion(suggestion: GroupedFiles, suggestionIndex: number) { |
| return html` |
| <li class="suggestion-row"> |
| <div |
| class="suggestion-row-indicator" |
| ?visible=${suggestion.hasSelected} |
| > |
| <gr-icon filled icon="check_circle"></gr-icon> |
| </div> |
| ${this.renderSuggestionGroupName(suggestion)} |
| ${this.renderSuggestionOwners(suggestion, suggestionIndex)} |
| </li> |
| `; |
| } |
| |
| private renderSuggestionGroupName(suggestion: GroupedFiles) { |
| return html` |
| <div class="suggestion-group-name"> |
| <div class="group-name-content"> |
| <span class="group-name"> ${suggestion.groupName.name} </span> |
| ${when( |
| suggestion.groupName.prefix, |
| () => html` |
| <span class="group-name-prefix"> |
| (${suggestion.groupName.prefix}) |
| </span> |
| ` |
| )} |
| ${when( |
| !suggestion.expanded, |
| () => html` |
| <gr-hovercard> |
| <owner-group-file-list |
| .files=${suggestion.files} |
| ></owner-group-file-list> |
| </gr-hovercard> |
| ` |
| )} |
| </div> |
| ${when( |
| suggestion.expanded, |
| () => html` |
| <owner-group-file-list |
| .files=${suggestion.files} |
| ></owner-group-file-list> |
| ` |
| )} |
| </div> |
| `; |
| } |
| |
| private renderSuggestionOwners( |
| suggestion: GroupedFiles, |
| suggestionIndex: number |
| ) { |
| if (suggestion.error) { |
| return html` <div class="fetch-error-content">${suggestion.error}</div> `; |
| } |
| |
| return html` |
| ${this.renderSuggestionOwnersNotFound(suggestion)} |
| ${this.renderSuggestedOwners(suggestion, suggestionIndex)} |
| `; |
| } |
| |
| private renderSuggestionOwnersNotFound(suggestion: GroupedFiles) { |
| if (this.areOwnersFound(suggestion.owners)) return nothing; |
| return html` |
| <div class="no-owners-content"> |
| <span>Not found</span> |
| <a |
| @click=${this.reportDocClick} |
| href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md#no-code-owners-found" |
| target="_blank" |
| > |
| <gr-icon icon="help" title="read documentation"></gr-icon> |
| </a> |
| </div> |
| `; |
| } |
| |
| private renderSuggestedOwners( |
| suggestion: GroupedFiles, |
| suggestionIndex: number |
| ) { |
| if (suggestion.owners?.owned_by_all_users) { |
| return html` |
| <div class="owned-by-all-users-content"> |
| <iron-icon icon="gr-icons:info"></iron-icon> |
| <span>${this.getOwnedByAllUsersContent()}</span> |
| </div> |
| `; |
| } |
| |
| return html` |
| ${when(this.showAllOwners, () => html`<div class="flex-break"></div>`)} |
| <ul class="suggested-owners"> |
| ${(suggestion.owners?.code_owners ?? []).map((owner, ownerIndex) => |
| this.renderSuggestedOwner(owner, suggestionIndex, ownerIndex) |
| )} |
| </ul> |
| `; |
| } |
| |
| private renderSuggestedOwner( |
| owner: CodeOwnerInfo, |
| suggestionIndex: number, |
| ownerIndex: number |
| ) { |
| return html`<gr-account-label |
| .account=${owner.account} |
| ?selected=${!!owner.selected} |
| @click=${() => this.toggleAccount(suggestionIndex, ownerIndex)} |
| ></gr-account-label>`; |
| } |
| |
| private onShowSuggestionsChanged() { |
| if (!this.showSuggestions) { |
| return; |
| } |
| // this is more of a hack to let review input lose focus |
| // to avoid suggestion dropdown |
| // gr-autocomplete has a internal state for tracking focus |
| // that will be canceled if any click happens outside of |
| // it's target |
| window.setTimeout(() => this.click(), 100); |
| } |
| |
| private onShowSuggestionsTypeChanged() { |
| if (!this.showSuggestions) { |
| this.modelLoader?.pauseActiveSuggestedOwnersLoading(); |
| return; |
| } |
| const selectedSuggestionsType = this.selectedSuggestionsType; |
| if (selectedSuggestionsType === undefined) { |
| return; |
| } |
| this.modelLoader?.loadSuggestions(selectedSuggestionsType); |
| // The progress is updated at the next _progressUpdateTimer tick. |
| // Without explicit call to updateLoadSuggestionsProgress it looks like |
| // a slow reaction to checkbox. |
| this.modelLoader?.updateLoadSelectedSuggestionsProgress(); |
| |
| if (!this.reportedEvents.get(selectedSuggestionsType)?.fetchingStart) { |
| this.reportedEvents.get(selectedSuggestionsType)!.fetchingStart = true; |
| this.reporting?.reportLifeCycle('owners-suggestions-fetching-start', { |
| type: selectedSuggestionsType, |
| }); |
| } |
| } |
| |
| _startUpdateProgressTimer() { |
| if (this.progressUpdateTimer) return; |
| this.progressUpdateTimer = window.setInterval(() => { |
| this.modelLoader?.updateLoadSelectedSuggestionsProgress(); |
| }, SUGGESTION_POLLING_INTERVAL); |
| } |
| |
| private stopUpdateProgressTimer() { |
| if (!this.progressUpdateTimer) return; |
| window.clearInterval(this.progressUpdateTimer); |
| this.progressUpdateTimer = undefined; |
| } |
| |
| private onSuggestionsStateChanged() { |
| this.stopUpdateProgressTimer(); |
| if (this.selectedSuggestionsState === SuggestionsState.Loading) { |
| this._startUpdateProgressTimer(); |
| } |
| } |
| |
| override loadPropertiesAfterModelChanged() { |
| super.loadPropertiesAfterModelChanged(); |
| this.stopUpdateProgressTimer(); |
| this.modelLoader?.loadAreAllFilesApproved(); |
| } |
| |
| private getReviewersIdSet(): Set<AccountId> { |
| return new Set( |
| (this.reviewers ?? []) |
| // Ugly hack, but we only care about accounts. |
| .map(account => (account as unknown as AccountInfo)._account_id) |
| .filter(account_id => account_id !== undefined) |
| ) as Set<AccountId>; |
| } |
| |
| private onSuggestionsFilesChanged() { |
| if ( |
| this.selectedSuggestionsFiles === undefined || |
| this.reviewers === undefined || |
| this.selectedSuggestionsType === undefined || |
| this.selectedSuggestionsState === undefined |
| ) |
| return; |
| |
| const allOwners = |
| this.model?.getSuggestionsByType(SuggestionsType.ALL_SUGGESTIONS) |
| ?.files ?? []; |
| |
| const allOwnersByPathMap = this.getOwnersByPathMap(allOwners); |
| const reviewersIdSet = this.getReviewersIdSet(); |
| const selectedSuggestionsType = this.selectedSuggestionsType; |
| const suggestionsState = this.selectedSuggestionsState; |
| |
| const groups = getDisplayOwnersGroups( |
| this.selectedSuggestionsFiles, |
| allOwnersByPathMap, |
| reviewersIdSet, |
| selectedSuggestionsType !== SuggestionsType.ALL_SUGGESTIONS |
| ); |
| // The updateLoadSuggestionsProgress method also updates suggestions |
| this.updateSuggestions(groups); |
| this.updateAllChips(this.reviewers); |
| |
| if (suggestionsState !== SuggestionsState.Loaded) return; |
| if (!this.reportedEvents.get(selectedSuggestionsType)!.fetchingFinished) { |
| this.reportedEvents.get(selectedSuggestionsType)!.fetchingFinished = true; |
| const reportDetails = groups.reduce( |
| (details, cur) => { |
| details.totalGroups++; |
| details.stats.push([ |
| cur.files.length, |
| cur.owners && cur.owners.code_owners |
| ? cur.owners.code_owners.length |
| : 0, |
| ]); |
| return details; |
| }, |
| { |
| totalGroups: 0, |
| stats: [] as Array<Array<number>>, |
| type: selectedSuggestionsType, |
| } |
| ); |
| this.reporting?.reportLifeCycle( |
| 'owners-suggestions-fetching-finished', |
| reportDetails |
| ); |
| } |
| } |
| |
| private getOwnersByPathMap(files?: Array<FetchedFile>) { |
| return new Map( |
| (files || []) |
| .filter(file => !file.info.error && file.info.owners) |
| .map(file => [file.path, file.info.owners]) |
| ); |
| } |
| |
| private updateSuggestions(suggestions: Array<GroupedFiles>) { |
| // update group names and files, no modification on owners or error |
| const suggestedOwners = suggestions.map(suggestion => |
| this.formatSuggestionInfo(suggestion) |
| ); |
| // move owned_by_all_users to the bottom: |
| const index = suggestedOwners.findIndex( |
| suggestion => suggestion.owners?.owned_by_all_users |
| ); |
| if (index >= 0) { |
| suggestedOwners.push(suggestedOwners.splice(index, 1)[0]); |
| } |
| this.suggestedOwners = suggestedOwners; |
| } |
| |
| onReviewerChanged() { |
| this.updateAllChips(this.reviewers); |
| } |
| |
| formatSuggestionInfo(suggestion: GroupedFiles): GroupedFiles { |
| const res = { |
| groupName: suggestion.groupName, |
| files: suggestion.files.slice(), |
| owners: undefined as CodeOwnersInfo | undefined, |
| error: undefined as Error | undefined, |
| } as GroupedFiles; |
| if (suggestion.owners) { |
| const codeOwners = (suggestion.owners.code_owners || []).map(owner => { |
| const updatedOwner = {...owner}; |
| const reviewers = this.change?.reviewers.REVIEWER; |
| if ( |
| reviewers && |
| reviewers.find( |
| reviewer => reviewer._account_id === owner.account?._account_id |
| ) |
| ) { |
| updatedOwner.selected = true; |
| } |
| return updatedOwner; |
| }); |
| res.owners = { |
| owned_by_all_users: !!suggestion.owners.owned_by_all_users, |
| code_owners: codeOwners, |
| }; |
| } else { |
| res.owners = { |
| owned_by_all_users: false, |
| code_owners: [], |
| }; |
| } |
| |
| res.error = suggestion.error; |
| return res; |
| } |
| |
| addAccount(owner: CodeOwnerInfo) { |
| owner.selected = true; |
| this.dispatchEvent( |
| new CustomEvent('add-reviewer', { |
| detail: { |
| reviewer: {...owner.account, _pendingAdd: true}, |
| }, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| this.reporting?.reportInteraction('add-reviewer'); |
| } |
| |
| removeAccount(owner: CodeOwnerInfo) { |
| owner.selected = false; |
| this.dispatchEvent( |
| new CustomEvent('remove-reviewer', { |
| detail: { |
| reviewer: {...owner.account, _pendingAdd: true}, |
| }, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| this.reporting?.reportInteraction('remove-reviewer'); |
| } |
| |
| toggleAccount(suggestionIndex: number, ownerIndex: number) { |
| const owner = |
| this.suggestedOwners![suggestionIndex]?.owners?.code_owners[ownerIndex]; |
| if (!owner) return; |
| if (owner.selected) { |
| this.removeAccount(owner); |
| } else { |
| this.addAccount(owner); |
| } |
| } |
| |
| private updateAllChips(accounts: Array<AccountInfo> | undefined) { |
| if (!this.suggestedOwners || !accounts) return; |
| // update all occurences |
| this.suggestedOwners.forEach((suggestion, sId) => { |
| let hasSelected = false; |
| suggestion.owners?.code_owners.forEach((owner, oId) => { |
| if ( |
| accounts.some( |
| account => account._account_id === owner!.account!._account_id |
| ) |
| ) { |
| this.suggestedOwners![sId].owners!.code_owners[oId].selected = true; |
| hasSelected = true; |
| } else { |
| this.suggestedOwners![sId].owners!.code_owners[oId].selected = false; |
| } |
| }); |
| const nonServiceUser = (account: AccountInfo) => |
| !account.tags || account.tags.indexOf('SERVICE_USER') < 0; |
| if ( |
| suggestion.owners?.owned_by_all_users && |
| accounts.some(nonServiceUser) |
| ) { |
| hasSelected = true; |
| } |
| this.suggestedOwners![sId].hasSelected = hasSelected; |
| }); |
| } |
| |
| private reportDocClick() { |
| this.reporting?.reportInteraction('code-owners-doc-click', { |
| section: 'no owners found', |
| }); |
| } |
| |
| private areOwnersFound(owners: CodeOwnersInfo | undefined) { |
| return ( |
| owners && (owners.code_owners.length > 0 || !!owners.owned_by_all_users) |
| ); |
| } |
| |
| private getOwnedByAllUsersContent() { |
| if (this.selectedSuggestionsState === SuggestionsState.Loading) { |
| return 'Any user can approve'; |
| } |
| // If all users own all the files in the change suggestedOwners.length === 1 |
| // (suggestedOwners - collection of owners groupbed by owners) |
| return this.suggestedOwners && this.suggestedOwners.length === 1 |
| ? 'Any user can approve. Please select a user manually' |
| : 'Any user from the other files can approve'; |
| } |
| |
| private showAllOwnersChanged() { |
| // The first call to this method happens before model is set. |
| if (!this.model) return; |
| this.model.setSelectedSuggestionType( |
| this.showAllOwners |
| ? SuggestionsType.ALL_SUGGESTIONS |
| : SuggestionsType.BEST_SUGGESTIONS |
| ); |
| } |
| } |