diff options
author | paladox <thomasmulhall410@yahoo.com> | 2024-04-23 15:24:07 +0100 |
---|---|---|
committer | Paladox none <thomasmulhall410@yahoo.com> | 2024-04-24 11:36:23 +0000 |
commit | c981357c32be97724651604bcb30650a3ee6e51c (patch) | |
tree | 3b7730cf4ede5f0cb5420a0c74c5b1ee59a26b3e | |
parent | 5491f3d6582d6cb9345ccbd9c108889288404331 (diff) |
gr-settings-view: Move preference to its own module/element
Also improves detecting unsaved changes so it matches diff/edit
preferences.
Release-Notes: gr-settings-view: Move preference to its own module/element
Change-Id: Ic7ee9909cd88228a8406a118b61bd340305ffd9a
(cherry picked from commit 7d57de663553c04ec6e94288658363af0090aa97)
4 files changed, 1066 insertions, 981 deletions
diff --git a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts new file mode 100644 index 0000000000..bb0f1e99cd --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts @@ -0,0 +1,598 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import '@polymer/iron-input/iron-input'; +import '../../shared/gr-button/gr-button'; +import '../../shared/gr-select/gr-select'; +import {AccountDetailInfo, PreferencesInput} from '../../../types/common'; +import {grFormStyles} from '../../../styles/gr-form-styles'; +import {menuPageStyles} from '../../../styles/gr-menu-page-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, html, css, nothing} from 'lit'; +import {customElement, query, state} from 'lit/decorators.js'; +import {convertToString} from '../../../utils/string-util'; +import {subscribe} from '../../lit/subscription-controller'; +import {resolve} from '../../../models/dependency'; +import {userModelToken} from '../../../models/user/user-model'; +import { + AppTheme, + DateFormat, + DiffViewMode, + EmailFormat, + EmailStrategy, + TimeFormat, +} from '../../../constants/constants'; +import {getAppContext} from '../../../services/app-context'; +import {KnownExperimentId} from '../../../services/flags/flags'; +import {areNotificationsEnabled} from '../../../utils/worker-util'; +import {getDocUrl} from '../../../utils/url-util'; +import {configModelToken} from '../../../models/config/config-model'; +import {SuggestionsProvider} from '../../../api/suggestions'; +import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader'; + +/** + * This provides an interface to show settings for a user profile + * as defined in PreferencesInfo. + */ +@customElement('gr-preferences') +export class GrPreferences extends LitElement { + @query('#themeSelect') themeSelect!: HTMLInputElement; + + @query('#changesPerPageSelect') changesPerPageSelect!: HTMLInputElement; + + @query('#dateTimeFormatSelect') dateTimeFormatSelect!: HTMLInputElement; + + @query('#timeFormatSelect') timeFormatSelect!: HTMLInputElement; + + @query('#emailNotificationsSelect') + emailNotificationsSelect!: HTMLInputElement; + + @query('#emailFormatSelect') emailFormatSelect!: HTMLInputElement; + + @query('#allowBrowserNotifications') + allowBrowserNotifications?: HTMLInputElement; + + @query('#allowSuggestCodeWhileCommenting') + allowSuggestCodeWhileCommenting?: HTMLInputElement; + + @query('#defaultBaseForMergesSelect') + defaultBaseForMergesSelect!: HTMLInputElement; + + @query('#relativeDateInChangeTable') + relativeDateInChangeTable!: HTMLInputElement; + + @query('#diffViewSelect') diffViewSelect!: HTMLInputElement; + + @query('#showSizeBarsInFileList') showSizeBarsInFileList!: HTMLInputElement; + + @query('#publishCommentsOnPush') publishCommentsOnPush!: HTMLInputElement; + + @query('#workInProgressByDefault') workInProgressByDefault!: HTMLInputElement; + + @query('#disableKeyboardShortcuts') + disableKeyboardShortcuts!: HTMLInputElement; + + @query('#disableTokenHighlighting') + disableTokenHighlighting!: HTMLInputElement; + + @query('#insertSignedOff') insertSignedOff!: HTMLInputElement; + + @state() prefs?: PreferencesInput; + + @state() private originalPrefs?: PreferencesInput; + + @state() account?: AccountDetailInfo; + + @state() private docsBaseUrl = ''; + + @state() + suggestionsProvider?: SuggestionsProvider; + + readonly getUserModel = resolve(this, userModelToken); + + private readonly getConfigModel = resolve(this, configModelToken); + + private readonly getPluginLoader = resolve(this, pluginLoaderToken); + + // private but used in test + readonly flagsService = getAppContext().flagsService; + + constructor() { + super(); + subscribe( + this, + () => this.getUserModel().preferences$, + prefs => { + this.originalPrefs = prefs; + this.prefs = {...prefs}; + } + ); + subscribe( + this, + () => this.getUserModel().account$, + acc => { + this.account = acc; + } + ); + subscribe( + this, + () => this.getConfigModel().docsBaseUrl$, + docsBaseUrl => (this.docsBaseUrl = docsBaseUrl) + ); + } + + override connectedCallback() { + super.connectedCallback(); + this.getPluginLoader() + .awaitPluginsLoaded() + .then(() => { + const suggestionsPlugins = + this.getPluginLoader().pluginsModel.getState().suggestionsPlugins; + // We currently support results from only 1 provider. + this.suggestionsProvider = suggestionsPlugins?.[0]?.provider; + }); + } + + static override get styles() { + return [ + sharedStyles, + menuPageStyles, + grFormStyles, + css` + :host { + border: none; + margin-bottom: var(--spacing-xxl); + } + h2 { + font-family: var(--header-font-family); + font-size: var(--font-size-h2); + font-weight: var(--font-weight-h2); + line-height: var(--line-height-h2); + } + `, + ]; + } + + override render() { + return html` + <h2 id="Preferences" class=${this.hasUnsavedChanges() ? 'edited' : ''}> + Preferences + </h2> + <fieldset id="preferences"> + <div id="preferences" class="gr-form-styles"> + <section> + <label class="title" for="themeSelect">Theme</label> + <span class="value"> + <gr-select + .bindValue=${this.prefs?.theme ?? AppTheme.AUTO} + @change=${() => { + this.prefs!.theme = this.themeSelect.value as AppTheme; + this.requestUpdate(); + }} + > + <select id="themeSelect"> + <option value="AUTO">Auto (based on OS prefs)</option> + <option value="LIGHT">Light</option> + <option value="DARK">Dark</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="changesPerPageSelect" + >Changes per page</label + > + <span class="value"> + <gr-select + .bindValue=${convertToString(this.prefs?.changes_per_page)} + @change=${() => { + this.prefs!.changes_per_page = Number( + this.changesPerPageSelect.value + ) as 10 | 25 | 50 | 100; + this.requestUpdate(); + }} + > + <select id="changesPerPageSelect"> + <option value="10">10 rows per page</option> + <option value="25">25 rows per page</option> + <option value="50">50 rows per page</option> + <option value="100">100 rows per page</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="dateTimeFormatSelect" + >Date/time format</label + > + <span class="value"> + <gr-select + .bindValue=${convertToString(this.prefs?.date_format)} + @change=${() => { + this.prefs!.date_format = this.dateTimeFormatSelect + .value as DateFormat; + this.requestUpdate(); + }} + > + <select id="dateTimeFormatSelect"> + <option value="STD">Jun 3 ; Jun 3, 2016</option> + <option value="US">06/03 ; 06/03/16</option> + <option value="ISO">06-03 ; 2016-06-03</option> + <option value="EURO">3. Jun ; 03.06.2016</option> + <option value="UK">03/06 ; 03/06/2016</option> + </select> + </gr-select> + <gr-select + .bindValue=${convertToString(this.prefs?.time_format)} + aria-label="Time Format" + @change=${() => { + this.prefs!.time_format = this.timeFormatSelect + .value as TimeFormat; + this.requestUpdate(); + }} + > + <select id="timeFormatSelect"> + <option value="HHMM_12">4:10 PM</option> + <option value="HHMM_24">16:10</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="emailNotificationsSelect" + >Email notifications</label + > + <span class="value"> + <gr-select + .bindValue=${convertToString(this.prefs?.email_strategy)} + @change=${() => { + this.prefs!.email_strategy = this.emailNotificationsSelect + .value as EmailStrategy; + this.requestUpdate(); + }} + > + <select id="emailNotificationsSelect"> + <option value="CC_ON_OWN_COMMENTS">Every comment</option> + <option value="ENABLED">Only comments left by others</option> + <option value="ATTENTION_SET_ONLY"> + Only when I am in the attention set + </option> + <option value="DISABLED">None</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="emailFormatSelect">Email format</label> + <span class="value"> + <gr-select + .bindValue=${convertToString(this.prefs?.email_format)} + @change=${() => { + this.prefs!.email_format = this.emailFormatSelect + .value as EmailFormat; + this.requestUpdate(); + }} + > + <select id="emailFormatSelect"> + <option value="HTML_PLAINTEXT">HTML and plaintext</option> + <option value="PLAINTEXT">Plaintext only</option> + </select> + </gr-select> + </span> + </section> + ${this.renderBrowserNotifications()} + ${this.renderGenerateSuggestionWhenCommenting()} + ${this.renderDefaultBaseForMerges()} + <section> + <label class="title" for="relativeDateInChangeTable" + >Show Relative Dates In Changes Table</label + > + <span class="value"> + <input + id="relativeDateInChangeTable" + type="checkbox" + ?checked=${this.prefs?.relative_date_in_change_table} + @change=${() => { + this.prefs!.relative_date_in_change_table = + this.relativeDateInChangeTable.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <span class="title">Diff view</span> + <span class="value"> + <gr-select + .bindValue=${convertToString(this.prefs?.diff_view)} + @change=${() => { + this.prefs!.diff_view = this.diffViewSelect + .value as DiffViewMode; + this.requestUpdate(); + }} + > + <select id="diffViewSelect"> + <option value="SIDE_BY_SIDE">Side by side</option> + <option value="UNIFIED_DIFF">Unified diff</option> + </select> + </gr-select> + </span> + </section> + <section> + <label for="showSizeBarsInFileList" class="title" + >Show size bars in file list</label + > + <span class="value"> + <input + id="showSizeBarsInFileList" + type="checkbox" + ?checked=${this.prefs?.size_bar_in_change_table} + @change=${() => { + this.prefs!.size_bar_in_change_table = + this.showSizeBarsInFileList.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <label for="publishCommentsOnPush" class="title" + >Publish comments on push</label + > + <span class="value"> + <input + id="publishCommentsOnPush" + type="checkbox" + ?checked=${this.prefs?.publish_comments_on_push} + @change=${() => { + this.prefs!.publish_comments_on_push = + this.publishCommentsOnPush.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <label for="workInProgressByDefault" class="title" + >Set new changes to "work in progress" by default</label + > + <span class="value"> + <input + id="workInProgressByDefault" + type="checkbox" + ?checked=${this.prefs?.work_in_progress_by_default} + @change=${() => { + this.prefs!.work_in_progress_by_default = + this.workInProgressByDefault.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <label for="disableKeyboardShortcuts" class="title" + >Disable all keyboard shortcuts</label + > + <span class="value"> + <input + id="disableKeyboardShortcuts" + type="checkbox" + ?checked=${this.prefs?.disable_keyboard_shortcuts} + @change=${() => { + this.prefs!.disable_keyboard_shortcuts = + this.disableKeyboardShortcuts.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <label for="disableTokenHighlighting" class="title" + >Disable token highlighting on hover</label + > + <span class="value"> + <input + id="disableTokenHighlighting" + type="checkbox" + ?checked=${this.prefs?.disable_token_highlighting} + @change=${() => { + this.prefs!.disable_token_highlighting = + this.disableTokenHighlighting.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + <section> + <label for="insertSignedOff" class="title"> + Insert Signed-off-by Footer For Inline Edit Changes + </label> + <span class="value"> + <input + id="insertSignedOff" + type="checkbox" + ?checked=${this.prefs?.signed_off_by} + @change=${() => { + this.prefs!.signed_off_by = this.insertSignedOff.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + </div> + <gr-button + id="savePrefs" + @click=${async () => { + await this.save(); + }} + ?disabled=${!this.hasUnsavedChanges()} + >Save changes</gr-button + > + </fieldset> + `; + } + + // When the experiment is over, move this back to render(), + // removing this function. + private renderBrowserNotifications() { + if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) + return nothing; + if ( + !this.flagsService.isEnabled( + KnownExperimentId.PUSH_NOTIFICATIONS_DEVELOPER + ) && + !areNotificationsEnabled(this.account) + ) + return nothing; + return html` <section id="allowBrowserNotificationsSection"> + <div class="title"> + <label for="allowBrowserNotifications" + >Allow browser notifications</label + > + <a + href=${getDocUrl( + this.docsBaseUrl, + 'user-attention-set.html#_browser_notifications' + )} + target="_blank" + rel="noopener noreferrer" + > + <gr-icon icon="help" title="read documentation"></gr-icon> + </a> + </div> + <span class="value"> + <input + id="allowBrowserNotifications" + type="checkbox" + ?checked=${this.prefs?.allow_browser_notifications} + @change=${() => { + this.prefs!.allow_browser_notifications = + this.allowBrowserNotifications!.checked; + this.requestUpdate(); + }} + /> + </span> + </section>`; + } + + // When the experiment is over, move this back to render(), + // removing this function. + private renderGenerateSuggestionWhenCommenting() { + if ( + !this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) || + !this.suggestionsProvider + ) + return nothing; + return html` + <section id="allowSuggestCodeWhileCommentingSection"> + <div class="title"> + <label for="allowSuggestCodeWhileCommenting" + >AI suggested fixes while commenting</label + > + <a + href=${this.suggestionsProvider.getDocumentationLink?.() || + getDocUrl( + this.docsBaseUrl, + 'user-suggest-edits.html#_generate_suggestion' + )} + target="_blank" + rel="noopener noreferrer" + > + <gr-icon icon="help" title="read documentation"></gr-icon> + </a> + </div> + <span class="value"> + <input + id="allowSuggestCodeWhileCommenting" + type="checkbox" + ?checked=${this.prefs?.allow_suggest_code_while_commenting} + @change=${() => { + this.prefs!.allow_suggest_code_while_commenting = + this.allowSuggestCodeWhileCommenting!.checked; + this.requestUpdate(); + }} + /> + </span> + </section> + `; + } + + // When this is fixed and can be re-enabled, move this back to render() + // and remove function. + private renderDefaultBaseForMerges() { + if (!this.prefs?.default_base_for_merges) return nothing; + return nothing; + // TODO: Re-enable respecting the default_base_for_merges preference. + // See corresponding TODO in change-model. + // return html` + // <section> + // <span class="title">Default Base For Merges</span> + // <span class="value"> + // <gr-select + // .bindValue=${convertToString( + // this.prefs?.default_base_for_merges + // )} + // @change=${() => { + // this.prefs!.default_base_for_merges = this + // .defaultBaseForMergesSelect.value as DefaultBase; + // this.requestUpdate(); + // }} + // > + // <select id="defaultBaseForMergesSelect"> + // <option value="AUTO_MERGE">Auto Merge</option> + // <option value="FIRST_PARENT">First Parent</option> + // </select> + // </gr-select> + // </span> + // </section> + // `; + } + + // private but used in test + hasUnsavedChanges() { + // We have to wrap boolean values in Boolean() to ensure undefined values + // use false rather than undefined. + return ( + this.originalPrefs?.theme !== this.prefs?.theme || + this.originalPrefs?.changes_per_page !== this.prefs?.changes_per_page || + this.originalPrefs?.date_format !== this.prefs?.date_format || + this.originalPrefs?.time_format !== this.prefs?.time_format || + this.originalPrefs?.email_strategy !== this.prefs?.email_strategy || + this.originalPrefs?.email_format !== this.prefs?.email_format || + Boolean(this.originalPrefs?.allow_browser_notifications) !== + Boolean(this.prefs?.allow_browser_notifications) || + Boolean(this.originalPrefs?.allow_suggest_code_while_commenting) !== + Boolean(this.prefs?.allow_suggest_code_while_commenting) || + this.originalPrefs?.default_base_for_merges !== + this.prefs?.default_base_for_merges || + Boolean(this.originalPrefs?.relative_date_in_change_table) !== + Boolean(this.prefs?.relative_date_in_change_table) || + this.originalPrefs?.diff_view !== this.prefs?.diff_view || + Boolean(this.originalPrefs?.size_bar_in_change_table) !== + Boolean(this.prefs?.size_bar_in_change_table) || + Boolean(this.originalPrefs?.publish_comments_on_push) !== + Boolean(this.prefs?.publish_comments_on_push) || + Boolean(this.originalPrefs?.work_in_progress_by_default) !== + Boolean(this.prefs?.work_in_progress_by_default) || + Boolean(this.originalPrefs?.disable_keyboard_shortcuts) !== + Boolean(this.prefs?.disable_keyboard_shortcuts) || + Boolean(this.originalPrefs?.disable_token_highlighting) !== + Boolean(this.prefs?.disable_token_highlighting) || + Boolean(this.originalPrefs?.signed_off_by) !== + Boolean(this.prefs?.signed_off_by) + ); + } + + async save() { + if (!this.prefs) return; + await this.getUserModel().updatePreferences(this.prefs); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'gr-preferences': GrPreferences; + } +} diff --git a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts new file mode 100644 index 0000000000..818af06ceb --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts @@ -0,0 +1,454 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import '../../../test/common-test-setup'; +import './gr-preferences'; +import { + queryAll, + queryAndAssert, + stubFlags, + stubRestApi, + waitUntil, +} from '../../../test/test-utils'; +import {GrPreferences} from './gr-preferences'; +import {PreferencesInfo, TopMenuItemInfo} from '../../../types/common'; +import { + AppTheme, + DateFormat, + DefaultBase, + DiffViewMode, + EmailFormat, + EmailStrategy, + TimeFormat, + createDefaultPreferences, +} from '../../../constants/constants'; +import {fixture, html, assert} from '@open-wc/testing'; +import {GrSelect} from '../../shared/gr-select/gr-select'; +import { + createAccountDetailWithId, + createPreferences, +} from '../../../test/test-data-generators'; + +suite('gr-preferences tests', () => { + let element: GrPreferences; + let preferences: PreferencesInfo; + + function valueOf(title: string, id: string): Element { + const sections = queryAll(element, `#${id} section`) ?? []; + let titleEl; + for (let i = 0; i < sections.length; i++) { + titleEl = sections[i].querySelector('.title'); + if (titleEl?.textContent?.trim() === title) { + const el = sections[i].querySelector('.value'); + if (el) return el; + } + } + assert.fail(`element with title ${title} not found`); + } + + setup(async () => { + preferences = { + ...createPreferences(), + changes_per_page: 25, + theme: AppTheme.LIGHT, + date_format: DateFormat.UK, + time_format: TimeFormat.HHMM_12, + diff_view: DiffViewMode.UNIFIED, + email_strategy: EmailStrategy.ENABLED, + email_format: EmailFormat.HTML_PLAINTEXT, + default_base_for_merges: DefaultBase.FIRST_PARENT, + relative_date_in_change_table: false, + size_bar_in_change_table: true, + my: [ + {url: '/first/url', name: 'first name', target: '_blank'}, + {url: '/second/url', name: 'second name', target: '_blank'}, + ] as TopMenuItemInfo[], + change_table: [], + }; + + stubRestApi('getPreferences').returns(Promise.resolve(preferences)); + + element = await fixture(html`<gr-preferences></gr-preferences>`); + + await element.updateComplete; + }); + + test('renders', () => { + assert.shadowDom.equal( + element, + /* HTML */ ` + <h2 id="Preferences">Preferences</h2> + <fieldset id="preferences"> + <div class="gr-form-styles" id="preferences"> + <section> + <label class="title" for="themeSelect"> Theme </label> + <span class="value"> + <gr-select> + <select id="themeSelect"> + <option value="AUTO">Auto (based on OS prefs)</option> + <option value="LIGHT">Light</option> + <option value="DARK">Dark</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="changesPerPageSelect"> + Changes per page + </label> + <span class="value"> + <gr-select> + <select id="changesPerPageSelect"> + <option value="10">10 rows per page</option> + <option value="25">25 rows per page</option> + <option value="50">50 rows per page</option> + <option value="100">100 rows per page</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="dateTimeFormatSelect"> + Date/time format + </label> + <span class="value"> + <gr-select> + <select id="dateTimeFormatSelect"> + <option value="STD">Jun 3 ; Jun 3, 2016</option> + <option value="US">06/03 ; 06/03/16</option> + <option value="ISO">06-03 ; 2016-06-03</option> + <option value="EURO">3. Jun ; 03.06.2016</option> + <option value="UK">03/06 ; 03/06/2016</option> + </select> + </gr-select> + <gr-select aria-label="Time Format"> + <select id="timeFormatSelect"> + <option value="HHMM_12">4:10 PM</option> + <option value="HHMM_24">16:10</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="emailNotificationsSelect"> + Email notifications + </label> + <span class="value"> + <gr-select> + <select id="emailNotificationsSelect"> + <option value="CC_ON_OWN_COMMENTS">Every comment</option> + <option value="ENABLED"> + Only comments left by others + </option> + <option value="ATTENTION_SET_ONLY"> + Only when I am in the attention set + </option> + <option value="DISABLED">None</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="emailFormatSelect"> + Email format + </label> + <span class="value"> + <gr-select> + <select id="emailFormatSelect"> + <option value="HTML_PLAINTEXT">HTML and plaintext</option> + <option value="PLAINTEXT">Plaintext only</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="relativeDateInChangeTable"> + Show Relative Dates In Changes Table + </label> + <span class="value"> + <input id="relativeDateInChangeTable" type="checkbox" /> + </span> + </section> + <section> + <span class="title"> Diff view </span> + <span class="value"> + <gr-select> + <select id="diffViewSelect"> + <option value="SIDE_BY_SIDE">Side by side</option> + <option value="UNIFIED_DIFF">Unified diff</option> + </select> + </gr-select> + </span> + </section> + <section> + <label class="title" for="showSizeBarsInFileList"> + Show size bars in file list + </label> + <span class="value"> + <input checked="" id="showSizeBarsInFileList" type="checkbox" /> + </span> + </section> + <section> + <label class="title" for="publishCommentsOnPush"> + Publish comments on push + </label> + <span class="value"> + <input id="publishCommentsOnPush" type="checkbox" /> + </span> + </section> + <section> + <label class="title" for="workInProgressByDefault"> + Set new changes to "work in progress" by default + </label> + <span class="value"> + <input id="workInProgressByDefault" type="checkbox" /> + </span> + </section> + <section> + <label class="title" for="disableKeyboardShortcuts"> + Disable all keyboard shortcuts + </label> + <span class="value"> + <input id="disableKeyboardShortcuts" type="checkbox" /> + </span> + </section> + <section> + <label class="title" for="disableTokenHighlighting"> + Disable token highlighting on hover + </label> + <span class="value"> + <input id="disableTokenHighlighting" type="checkbox" /> + </span> + </section> + <section> + <label class="title" for="insertSignedOff"> + Insert Signed-off-by Footer For Inline Edit Changes + </label> + <span class="value"> + <input id="insertSignedOff" type="checkbox" /> + </span> + </section> + </div> + <gr-button + aria-disabled="true" + disabled="" + id="savePrefs" + role="button" + tabindex="-1" + > + Save changes + </gr-button> + </fieldset> + ` + ); + }); + + test('allow browser notifications', async () => { + stubFlags('isEnabled').returns(true); + element.account = createAccountDetailWithId(); + await element.updateComplete; + assert.dom.equal( + queryAndAssert(element, '#allowBrowserNotificationsSection'), + /* HTML */ `<section id="allowBrowserNotificationsSection"> + <div class="title"> + <label for="allowBrowserNotifications"> + Allow browser notifications + </label> + <a + href="/Documentation/user-attention-set.html#_browser_notifications" + target="_blank" + rel="noopener noreferrer" + > + <gr-icon icon="help" title="read documentation"> </gr-icon> + </a> + </div> + <span class="value"> + <input checked="" id="allowBrowserNotifications" type="checkbox" /> + </span> + </section>` + ); + }); + + test('input values match preferences', () => { + // Rendered with the expected preferences selected. + assert.equal( + Number( + ( + valueOf('Changes per page', 'preferences')! + .firstElementChild as GrSelect + ).bindValue + ), + preferences.changes_per_page + ); + assert.equal( + (valueOf('Theme', 'preferences').firstElementChild as GrSelect).bindValue, + preferences.theme + ); + assert.equal( + ( + valueOf('Date/time format', 'preferences')! + .firstElementChild as GrSelect + ).bindValue, + preferences.date_format + ); + assert.equal( + (valueOf('Date/time format', 'preferences')!.lastElementChild as GrSelect) + .bindValue, + preferences.time_format + ); + assert.equal( + ( + valueOf('Email notifications', 'preferences')! + .firstElementChild as GrSelect + ).bindValue, + preferences.email_strategy + ); + assert.equal( + (valueOf('Email format', 'preferences')!.firstElementChild as GrSelect) + .bindValue, + preferences.email_format + ); + assert.equal( + ( + valueOf('Show Relative Dates In Changes Table', 'preferences')! + .firstElementChild as HTMLInputElement + ).checked, + false + ); + assert.equal( + (valueOf('Diff view', 'preferences')!.firstElementChild as GrSelect) + .bindValue, + preferences.diff_view + ); + assert.equal( + ( + valueOf('Show size bars in file list', 'preferences')! + .firstElementChild as HTMLInputElement + ).checked, + true + ); + assert.equal( + ( + valueOf('Publish comments on push', 'preferences')! + .firstElementChild as HTMLInputElement + ).checked, + false + ); + assert.equal( + ( + valueOf( + 'Set new changes to "work in progress" by default', + 'preferences' + )!.firstElementChild as HTMLInputElement + ).checked, + false + ); + assert.equal( + ( + valueOf('Disable token highlighting on hover', 'preferences')! + .firstElementChild as HTMLInputElement + ).checked, + false + ); + assert.equal( + ( + valueOf( + 'Insert Signed-off-by Footer For Inline Edit Changes', + 'preferences' + )!.firstElementChild as HTMLInputElement + ).checked, + false + ); + + assert.isFalse(element.hasUnsavedChanges()); + }); + + test('save changes', async () => { + assert.equal(element.prefs?.theme, AppTheme.LIGHT); + + const themeSelect = valueOf('Theme', 'preferences') + .firstElementChild as GrSelect; + themeSelect.bindValue = AppTheme.DARK; + + themeSelect.dispatchEvent( + new CustomEvent('change', { + composed: true, + bubbles: true, + }) + ); + + const publishOnPush = valueOf('Publish comments on push', 'preferences')! + .firstElementChild! as HTMLSpanElement; + + publishOnPush.click(); + + assert.isTrue(element.hasUnsavedChanges()); + + const savePrefStub = stubRestApi('savePreferences').resolves( + element.prefs as PreferencesInfo + ); + + await element.save(); + + // Wait for model state update, since this is not awaited by element.save() + await waitUntil( + () => + element.getUserModel().getState().preferences?.theme === AppTheme.DARK + ); + await waitUntil( + () => element.getUserModel().getState().preferences?.my === preferences.my + ); + await waitUntil( + () => + element.getUserModel().getState().preferences + ?.publish_comments_on_push === true + ); + + assert.isTrue(savePrefStub.called); + assert.isFalse(element.hasUnsavedChanges()); + }); + + test('publish comments on push', async () => { + assert.isFalse(element.hasUnsavedChanges()); + + const publishCommentsOnPush = valueOf( + 'Publish comments on push', + 'preferences' + )!.firstElementChild! as HTMLSpanElement; + publishCommentsOnPush.click(); + + assert.isTrue(element.hasUnsavedChanges()); + + stubRestApi('savePreferences').callsFake(prefs => { + assert.equal(prefs.publish_comments_on_push, true); + return Promise.resolve(createDefaultPreferences()); + }); + + // Save the change. + await element.save(); + assert.isFalse(element.hasUnsavedChanges()); + }); + + test('set new changes work-in-progress', async () => { + assert.isFalse(element.hasUnsavedChanges()); + + const newChangesWorkInProgress = valueOf( + 'Set new changes to "work in progress" by default', + 'preferences' + )!.firstElementChild! as HTMLSpanElement; + newChangesWorkInProgress.click(); + + assert.isTrue(element.hasUnsavedChanges()); + + stubRestApi('savePreferences').callsFake(prefs => { + assert.equal(prefs.work_in_progress_by_default, true); + return Promise.resolve(createDefaultPreferences()); + }); + + // Save the change. + await element.save(); + assert.isFalse(element.hasUnsavedChanges()); + }); +}); diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts index b002fc8b95..fd7d67b520 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts @@ -21,6 +21,7 @@ import '../gr-group-list/gr-group-list'; import '../gr-http-password/gr-http-password'; import '../gr-identities/gr-identities'; import '../gr-menu-editor/gr-menu-editor'; +import '../gr-preferences/gr-preferences'; import '../gr-ssh-editor/gr-ssh-editor'; import '../gr-watched-projects-editor/gr-watched-projects-editor'; import '../../shared/gr-dialog/gr-dialog'; @@ -39,17 +40,8 @@ import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor'; import {GrEmailEditor} from '../gr-email-editor/gr-email-editor'; import {fireAlert, fireTitleChange} from '../../../utils/event-util'; import {getAppContext} from '../../../services/app-context'; -import { - DateFormat, - DefaultBase, - DiffViewMode, - EmailFormat, - EmailStrategy, - AppTheme, - TimeFormat, -} from '../../../constants/constants'; import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events'; -import {LitElement, css, html, nothing} from 'lit'; +import {LitElement, css, html} from 'lit'; import {customElement, query, queryAsync, state} from 'lit/decorators.js'; import {sharedStyles} from '../../../styles/shared-styles'; import {paperStyles} from '../../../styles/gr-paper-styles'; @@ -58,29 +50,24 @@ import {when} from 'lit/directives/when.js'; import {pageNavStyles} from '../../../styles/gr-page-nav-styles'; import {menuPageStyles} from '../../../styles/gr-menu-page-styles'; import {grFormStyles} from '../../../styles/gr-form-styles'; -import {KnownExperimentId} from '../../../services/flags/flags'; import {subscribe} from '../../lit/subscription-controller'; import {resolve} from '../../../models/dependency'; import {settingsViewModelToken} from '../../../models/views/settings'; -import {areNotificationsEnabled} from '../../../utils/worker-util'; import { changeTablePrefs, userModelToken, } from '../../../models/user/user-model'; import {modalStyles} from '../../../styles/gr-modal-styles'; import {navigationToken} from '../../core/gr-navigation/gr-navigation'; -import {getDocUrl, rootUrl} from '../../../utils/url-util'; -import {configModelToken} from '../../../models/config/config-model'; -import {SuggestionsProvider} from '../../../api/suggestions'; -import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader'; +import {rootUrl} from '../../../utils/url-util'; const HTTP_AUTH = ['HTTP', 'HTTP_LDAP']; -enum CopyPrefsDirection { - PrefsToLocalPrefs, - LocalPrefsToPrefs, -} - +/** + * This provides an interface to show all settings for a user profile. + * In most cases a individual module is used per setting to make + * code more readable. In other cases, it is created within this module. + */ @customElement('gr-settings-view') export class GrSettingsView extends LitElement { /** @@ -109,64 +96,17 @@ export class GrSettingsView extends LitElement { @query('#emailEditor', true) emailEditor!: GrEmailEditor; - @query('#insertSignedOff') insertSignedOff!: HTMLInputElement; - - @query('#workInProgressByDefault') workInProgressByDefault!: HTMLInputElement; - - @query('#showSizeBarsInFileList') showSizeBarsInFileList!: HTMLInputElement; - - @query('#publishCommentsOnPush') publishCommentsOnPush!: HTMLInputElement; - - @query('#allowBrowserNotifications') - allowBrowserNotifications?: HTMLInputElement; - - @query('#allowSuggestCodeWhileCommenting') - allowSuggestCodeWhileCommenting?: HTMLInputElement; - - @query('#disableKeyboardShortcuts') - disableKeyboardShortcuts!: HTMLInputElement; - - @query('#disableTokenHighlighting') - disableTokenHighlighting!: HTMLInputElement; - - @query('#relativeDateInChangeTable') - relativeDateInChangeTable!: HTMLInputElement; - - @query('#changesPerPageSelect') changesPerPageSelect!: HTMLInputElement; - - @query('#dateTimeFormatSelect') dateTimeFormatSelect!: HTMLInputElement; - - @query('#timeFormatSelect') timeFormatSelect!: HTMLInputElement; - - @query('#emailNotificationsSelect') - emailNotificationsSelect!: HTMLInputElement; - - @query('#emailFormatSelect') emailFormatSelect!: HTMLInputElement; - - @query('#defaultBaseForMergesSelect') - defaultBaseForMergesSelect!: HTMLInputElement; - - @query('#diffViewSelect') diffViewSelect!: HTMLInputElement; - - @query('#themeSelect') themeSelect!: HTMLInputElement; - @state() prefs: PreferencesInput = {}; @state() private accountInfoChanged = false; // private but used in test - @state() localPrefs: PreferencesInput = {}; - - // private but used in test @state() localChangeTableColumns: string[] = []; @state() private loading = true; @state() private changeTableChanged = false; - // private but used in test - @state() prefsChanged = false; - @state() private diffPrefsChanged = false; @state() private watchedProjectsChanged = false; @@ -199,11 +139,6 @@ export class GrSettingsView extends LitElement { @state() isDeletingAccount = false; - @state() private docsBaseUrl = ''; - - @state() - suggestionsProvider?: SuggestionsProvider; - // private but used in test public _testOnly_loadingPromise?: Promise<void>; @@ -218,10 +153,6 @@ export class GrSettingsView extends LitElement { private readonly getNavigation = resolve(this, navigationToken); - private readonly getConfigModel = resolve(this, configModelToken); - - private readonly getPluginLoader = resolve(this, pluginLoaderToken); - constructor() { super(); subscribe( @@ -248,16 +179,9 @@ export class GrSettingsView extends LitElement { } this.prefs = prefs; this.showNumber = !!prefs.legacycid_in_change_table; - this.copyPrefs(CopyPrefsDirection.PrefsToLocalPrefs); - this.prefsChanged = false; this.localChangeTableColumns = changeTablePrefs(prefs); } ); - subscribe( - this, - () => this.getConfigModel().docsBaseUrl$, - docsBaseUrl => (this.docsBaseUrl = docsBaseUrl) - ); } // private, but used in tests @@ -275,14 +199,6 @@ export class GrSettingsView extends LitElement { // we need to manually calling scrollIntoView when hash changed document.addEventListener('location-change', this.handleLocationChange); fireTitleChange('Settings'); - this.getPluginLoader() - .awaitPluginsLoaded() - .then(() => { - const suggestionsPlugins = - this.getPluginLoader().pluginsModel.getState().suggestionsPlugins; - // We currently support results from only 1 provider. - this.suggestionsProvider = suggestionsPlugins?.[0]?.provider; - }); } override firstUpdated() { @@ -459,32 +375,7 @@ export class GrSettingsView extends LitElement { </gr-dialog> </dialog> </fieldset> - <h2 - id="Preferences" - class=${this.computeHeaderClass(this.prefsChanged)} - > - Preferences - </h2> - <fieldset id="preferences"> - ${this.renderTheme()} ${this.renderChangesPerPages()} - ${this.renderDateTimeFormat()} ${this.renderEmailNotification()} - ${this.renderEmailFormat()} ${this.renderBrowserNotifications()} - ${this.renderGenerateSuggestionWhenCommenting()} - ${this.renderDefaultBaseForMerges()} - ${this.renderRelativeDateInChangeTable()} ${this.renderDiffView()} - ${this.renderShowSizeBarsInFileList()} - ${this.renderPublishCommentsOnPush()} - ${this.renderWorkInProgressByDefault()} - ${this.renderDisableKeyboardShortcuts()} - ${this.renderDisableTokenHighlighting()} - ${this.renderInsertSignedOff()} - <gr-button - id="savePrefs" - @click=${this.handleSavePreferences} - ?disabled=${!this.prefsChanged} - >Save changes</gr-button - > - </fieldset> + <gr-preferences id="preferences"></gr-preferences> <h2 id="DiffPreferences" class=${this.computeHeaderClass(this.diffPrefsChanged)} @@ -701,436 +592,6 @@ export class GrSettingsView extends LitElement { super.disconnectedCallback(); } - private renderTheme() { - return html` - <section> - <label class="title" for="themeSelect">Theme</label> - <span class="value"> - <gr-select - .bindValue=${this.localPrefs.theme ?? AppTheme.AUTO} - @change=${() => { - this.localPrefs.theme = this.themeSelect.value as AppTheme; - this.prefsChanged = true; - }} - > - <select id="themeSelect"> - <option value="AUTO">Auto (based on OS prefs)</option> - <option value="LIGHT">Light</option> - <option value="DARK">Dark</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderChangesPerPages() { - return html` - <section> - <label class="title" for="changesPerPageSelect">Changes per page</label> - <span class="value"> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.changes_per_page)} - @change=${() => { - this.localPrefs.changes_per_page = Number( - this.changesPerPageSelect.value - ) as 10 | 25 | 50 | 100; - this.prefsChanged = true; - }} - > - <select id="changesPerPageSelect"> - <option value="10">10 rows per page</option> - <option value="25">25 rows per page</option> - <option value="50">50 rows per page</option> - <option value="100">100 rows per page</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderDateTimeFormat() { - return html` - <section> - <label class="title" for="dateTimeFormatSelect">Date/time format</label> - <span class="value"> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.date_format)} - @change=${() => { - this.localPrefs.date_format = this.dateTimeFormatSelect - .value as DateFormat; - this.prefsChanged = true; - }} - > - <select id="dateTimeFormatSelect"> - <option value="STD">Jun 3 ; Jun 3, 2016</option> - <option value="US">06/03 ; 06/03/16</option> - <option value="ISO">06-03 ; 2016-06-03</option> - <option value="EURO">3. Jun ; 03.06.2016</option> - <option value="UK">03/06 ; 03/06/2016</option> - </select> - </gr-select> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.time_format)} - aria-label="Time Format" - @change=${() => { - this.localPrefs.time_format = this.timeFormatSelect - .value as TimeFormat; - this.prefsChanged = true; - }} - > - <select id="timeFormatSelect"> - <option value="HHMM_12">4:10 PM</option> - <option value="HHMM_24">16:10</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderEmailNotification() { - return html` - <section> - <label class="title" for="emailNotificationsSelect" - >Email notifications</label - > - <span class="value"> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.email_strategy)} - @change=${() => { - this.localPrefs.email_strategy = this.emailNotificationsSelect - .value as EmailStrategy; - this.prefsChanged = true; - }} - > - <select id="emailNotificationsSelect"> - <option value="CC_ON_OWN_COMMENTS">Every comment</option> - <option value="ENABLED">Only comments left by others</option> - <option value="ATTENTION_SET_ONLY"> - Only when I am in the attention set - </option> - <option value="DISABLED">None</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderEmailFormat() { - if (!this.localPrefs.email_format) return nothing; - return html` - <section> - <label class="title" for="emailFormatSelect">Email format</label> - <span class="value"> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.email_format)} - @change=${() => { - this.localPrefs.email_format = this.emailFormatSelect - .value as EmailFormat; - this.prefsChanged = true; - }} - > - <select id="emailFormatSelect"> - <option value="HTML_PLAINTEXT">HTML and plaintext</option> - <option value="PLAINTEXT">Plaintext only</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderBrowserNotifications() { - if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) - return nothing; - if ( - !this.flagsService.isEnabled( - KnownExperimentId.PUSH_NOTIFICATIONS_DEVELOPER - ) && - !areNotificationsEnabled(this.account) - ) - return nothing; - return html` - <section id="allowBrowserNotificationsSection"> - <div class="title"> - <label for="allowBrowserNotifications" - >Allow browser notifications</label - > - <a - href=${getDocUrl( - this.docsBaseUrl, - 'user-attention-set.html#_browser_notifications' - )} - target="_blank" - rel="noopener noreferrer" - > - <gr-icon icon="help" title="read documentation"></gr-icon> - </a> - </div> - <span class="value"> - <input - id="allowBrowserNotifications" - type="checkbox" - ?checked=${this.localPrefs.allow_browser_notifications} - @change=${() => { - this.localPrefs.allow_browser_notifications = - this.allowBrowserNotifications!.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderGenerateSuggestionWhenCommenting() { - if ( - !this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) || - !this.suggestionsProvider - ) - return nothing; - return html` - <section id="allowSuggestCodeWhileCommentingSection"> - <div class="title"> - <label for="allowSuggestCodeWhileCommenting" - >AI suggested fixes while commenting</label - > - <a - href=${this.suggestionsProvider.getDocumentationLink?.() || - getDocUrl( - this.docsBaseUrl, - 'user-suggest-edits.html#_generate_suggestion' - )} - target="_blank" - rel="noopener noreferrer" - > - <gr-icon icon="help" title="read documentation"></gr-icon> - </a> - </div> - <span class="value"> - <input - id="allowSuggestCodeWhileCommenting" - type="checkbox" - ?checked=${this.localPrefs.allow_suggest_code_while_commenting} - @change=${() => { - this.localPrefs.allow_suggest_code_while_commenting = - this.allowSuggestCodeWhileCommenting!.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderDefaultBaseForMerges() { - if (!this.localPrefs.default_base_for_merges) return nothing; - return nothing; - // TODO: Re-enable respecting the default_base_for_merges preference. - // See corresponding TODO in change-model. - // return html` - // <section> - // <span class="title">Default Base For Merges</span> - // <span class="value"> - // <gr-select - // .bindValue=${this.convertToString( - // this.localPrefs.default_base_for_merges - // )} - // @change=${() => { - // this.localPrefs.default_base_for_merges = this - // .defaultBaseForMergesSelect.value as DefaultBase; - // this.prefsChanged = true; - // }} - // > - // <select id="defaultBaseForMergesSelect"> - // <option value="AUTO_MERGE">Auto Merge</option> - // <option value="FIRST_PARENT">First Parent</option> - // </select> - // </gr-select> - // </span> - // </section> - // `; - } - - private renderRelativeDateInChangeTable() { - return html` - <section> - <label class="title" for="relativeDateInChangeTable" - >Show Relative Dates In Changes Table</label - > - <span class="value"> - <input - id="relativeDateInChangeTable" - type="checkbox" - ?checked=${this.localPrefs.relative_date_in_change_table} - @change=${() => { - this.localPrefs.relative_date_in_change_table = - this.relativeDateInChangeTable.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderDiffView() { - return html` - <section> - <span class="title">Diff view</span> - <span class="value"> - <gr-select - .bindValue=${this.convertToString(this.localPrefs.diff_view)} - @change=${() => { - this.localPrefs.diff_view = this.diffViewSelect - .value as DiffViewMode; - this.prefsChanged = true; - }} - > - <select id="diffViewSelect"> - <option value="SIDE_BY_SIDE">Side by side</option> - <option value="UNIFIED_DIFF">Unified diff</option> - </select> - </gr-select> - </span> - </section> - `; - } - - private renderShowSizeBarsInFileList() { - return html` - <section> - <label for="showSizeBarsInFileList" class="title" - >Show size bars in file list</label - > - <span class="value"> - <input - id="showSizeBarsInFileList" - type="checkbox" - ?checked=${this.localPrefs.size_bar_in_change_table} - @change=${() => { - this.localPrefs.size_bar_in_change_table = - this.showSizeBarsInFileList.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderPublishCommentsOnPush() { - return html` - <section> - <label for="publishCommentsOnPush" class="title" - >Publish comments on push</label - > - <span class="value"> - <input - id="publishCommentsOnPush" - type="checkbox" - ?checked=${this.localPrefs.publish_comments_on_push} - @change=${() => { - this.localPrefs.publish_comments_on_push = - this.publishCommentsOnPush.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderWorkInProgressByDefault() { - return html` - <section> - <label for="workInProgressByDefault" class="title" - >Set new changes to "work in progress" by default</label - > - <span class="value"> - <input - id="workInProgressByDefault" - type="checkbox" - ?checked=${this.localPrefs.work_in_progress_by_default} - @change=${() => { - this.localPrefs.work_in_progress_by_default = - this.workInProgressByDefault.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderDisableKeyboardShortcuts() { - return html` - <section> - <label for="disableKeyboardShortcuts" class="title" - >Disable all keyboard shortcuts</label - > - <span class="value"> - <input - id="disableKeyboardShortcuts" - type="checkbox" - ?checked=${this.localPrefs.disable_keyboard_shortcuts} - @change=${() => { - this.localPrefs.disable_keyboard_shortcuts = - this.disableKeyboardShortcuts.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderDisableTokenHighlighting() { - return html` - <section> - <label for="disableTokenHighlighting" class="title" - >Disable token highlighting on hover</label - > - <span class="value"> - <input - id="disableTokenHighlighting" - type="checkbox" - ?checked=${this.localPrefs.disable_token_highlighting} - @change=${() => { - this.localPrefs.disable_token_highlighting = - this.disableTokenHighlighting.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - - private renderInsertSignedOff() { - return html` - <section> - <label for="insertSignedOff" class="title"> - Insert Signed-off-by Footer For Inline Edit Changes - </label> - <span class="value"> - <input - id="insertSignedOff" - type="checkbox" - ?checked=${this.localPrefs.signed_off_by} - @change=${() => { - this.localPrefs.signed_off_by = this.insertSignedOff.checked; - this.prefsChanged = true; - }} - /> - </span> - </section> - `; - } - private readonly handleLocationChange = () => { // Handle anchor tag after dom attached const urlHash = window.location.hash; @@ -1147,30 +608,12 @@ export class GrSettingsView extends LitElement { Promise.all([this.accountInfo.loadData(), this.emailEditor.loadData()]); } - private copyPrefs(direction: CopyPrefsDirection) { - if (direction === CopyPrefsDirection.LocalPrefsToPrefs) { - this.prefs = { - ...this.localPrefs, - }; - } else { - this.localPrefs = { - ...this.prefs, - }; - } - } - - // private but used in test - handleSavePreferences() { - return this.getUserModel().updatePreferences(this.localPrefs); - } - // private but used in test - handleSaveChangeTable() { + async handleSaveChangeTable() { this.prefs.change_table = this.localChangeTableColumns; this.prefs.legacycid_in_change_table = this.showNumber; - return this.restApiService.savePreferences(this.prefs).then(() => { - this.changeTableChanged = false; - }); + await this.getUserModel().updatePreferences(this.prefs); + this.changeTableChanged = false; } private computeHeaderClass(changed?: boolean) { @@ -1235,25 +678,6 @@ export class GrSettingsView extends LitElement { return false; } - - /** - * bind-value has type string so we have to convert anything inputed - * to string. - * - * This is so typescript template checker doesn't fail. - */ - private convertToString( - key?: - | DateFormat - | DefaultBase - | DiffViewMode - | EmailFormat - | EmailStrategy - | TimeFormat - | number - ) { - return key !== undefined ? String(key) : ''; - } } declare global { diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts index f9b1738de8..b6690b6a0d 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts @@ -6,13 +6,7 @@ import '../../../test/common-test-setup'; import './gr-settings-view'; import {GrSettingsView} from './gr-settings-view'; -import { - queryAll, - queryAndAssert, - stubFlags, - stubRestApi, - waitEventLoop, -} from '../../../test/test-utils'; +import {stubRestApi, waitEventLoop} from '../../../test/test-utils'; import { AuthInfo, AccountDetailInfo, @@ -22,7 +16,6 @@ import { TopMenuItemInfo, } from '../../../types/common'; import { - createDefaultPreferences, DateFormat, DefaultBase, DiffViewMode, @@ -36,7 +29,6 @@ import { createPreferences, createServerInfo, } from '../../../test/test-data-generators'; -import {GrSelect} from '../../shared/gr-select/gr-select'; import {fixture, html, assert} from '@open-wc/testing'; suite('gr-settings-view tests', () => { @@ -45,36 +37,6 @@ suite('gr-settings-view tests', () => { let preferences: PreferencesInfo; let config: ServerInfo; - function valueOf(title: string, id: string) { - const sections = queryAll(element, `#${id} section`); - let titleEl; - for (let i = 0; i < sections.length; i++) { - titleEl = sections[i].querySelector('.title'); - if (titleEl?.textContent?.trim() === title) { - const el = sections[i].querySelector('.value'); - if (el) return el; - } - } - assert.fail(`element with title ${title} not found`); - } - - // Because deepEqual isn't behaving in Safari. - function assertMenusEqual( - actual?: TopMenuItemInfo[], - expected?: TopMenuItemInfo[] - ) { - if (actual === undefined) { - assert.fail("assertMenusEqual 'actual' param is undefined"); - } else if (expected === undefined) { - assert.fail("assertMenusEqual 'expected' param is undefined"); - } - assert.equal(actual.length, expected.length); - for (let i = 0; i < actual.length; i++) { - assert.equal(actual[i].name, expected[i].name); - assert.equal(actual[i].url, expected[i].url); - } - } - function stubAddAccountEmail(statusCode: number) { return stubRestApi('addAccountEmail').callsFake(() => Promise.resolve({status: statusCode} as Response) @@ -193,172 +155,7 @@ suite('gr-settings-view tests', () => { </gr-dialog> </dialog> </fieldset> - <h2 id="Preferences">Preferences</h2> - <fieldset id="preferences"> - <section> - <label class="title" for="themeSelect"> - Theme - </label> - <span class="value"> - <gr-select> - <select id="themeSelect"> - <option value="AUTO">Auto (based on OS prefs)</option> - <option value="LIGHT">Light</option> - <option value="DARK">Dark</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="changesPerPageSelect"> - Changes per page - </label> - <span class="value"> - <gr-select> - <select id="changesPerPageSelect"> - <option value="10">10 rows per page</option> - <option value="25">25 rows per page</option> - <option value="50">50 rows per page</option> - <option value="100">100 rows per page</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="dateTimeFormatSelect"> - Date/time format - </label> - <span class="value"> - <gr-select> - <select id="dateTimeFormatSelect"> - <option value="STD">Jun 3 ; Jun 3, 2016</option> - <option value="US">06/03 ; 06/03/16</option> - <option value="ISO">06-03 ; 2016-06-03</option> - <option value="EURO">3. Jun ; 03.06.2016</option> - <option value="UK">03/06 ; 03/06/2016</option> - </select> - </gr-select> - <gr-select aria-label="Time Format"> - <select id="timeFormatSelect"> - <option value="HHMM_12">4:10 PM</option> - <option value="HHMM_24">16:10</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="emailNotificationsSelect"> - Email notifications - </label> - <span class="value"> - <gr-select> - <select id="emailNotificationsSelect"> - <option value="CC_ON_OWN_COMMENTS">Every comment</option> - <option value="ENABLED"> - Only comments left by others - </option> - <option value="ATTENTION_SET_ONLY"> - Only when I am in the attention set - </option> - <option value="DISABLED">None</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="emailFormatSelect"> - Email format - </label> - <span class="value"> - <gr-select> - <select id="emailFormatSelect"> - <option value="HTML_PLAINTEXT">HTML and plaintext</option> - <option value="PLAINTEXT">Plaintext only</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="relativeDateInChangeTable"> - Show Relative Dates In Changes Table - </label> - <span class="value"> - <input id="relativeDateInChangeTable" type="checkbox" /> - </span> - </section> - <section> - <span class="title"> Diff view </span> - <span class="value"> - <gr-select> - <select id="diffViewSelect"> - <option value="SIDE_BY_SIDE">Side by side</option> - <option value="UNIFIED_DIFF">Unified diff</option> - </select> - </gr-select> - </span> - </section> - <section> - <label class="title" for="showSizeBarsInFileList"> - Show size bars in file list - </label> - <span class="value"> - <input - checked="" - id="showSizeBarsInFileList" - type="checkbox" - /> - </span> - </section> - <section> - <label class="title" for="publishCommentsOnPush"> - Publish comments on push - </label> - <span class="value"> - <input id="publishCommentsOnPush" type="checkbox" /> - </span> - </section> - <section> - <label class="title" for="workInProgressByDefault"> - Set new changes to "work in progress" by default - </label> - <span class="value"> - <input id="workInProgressByDefault" type="checkbox" /> - </span> - </section> - <section> - <label class="title" for="disableKeyboardShortcuts"> - Disable all keyboard shortcuts - </label> - <span class="value"> - <input id="disableKeyboardShortcuts" type="checkbox" /> - </span> - </section> - <section> - <label class="title" for="disableTokenHighlighting"> - Disable token highlighting on hover - </label> - <span class="value"> - <input id="disableTokenHighlighting" type="checkbox" /> - </span> - </section> - <section> - <label class="title" for="insertSignedOff"> - Insert Signed-off-by Footer For Inline Edit Changes - </label> - <span class="value"> - <input id="insertSignedOff" type="checkbox" /> - </span> - </section> - <gr-button - aria-disabled="true" - disabled="" - id="savePrefs" - role="button" - tabindex="-1" - > - Save changes - </gr-button> - </fieldset> + <gr-preferences id="preferences"> </gr-preferences> <h2 id="DiffPreferences">Diff Preferences</h2> <fieldset id="diffPreferences"> <gr-diff-preferences id="diffPrefs"> </gr-diff-preferences> @@ -456,33 +253,6 @@ suite('gr-settings-view tests', () => { ); }); - test('allow browser notifications', async () => { - stubFlags('isEnabled').returns(true); - element = await fixture(html`<gr-settings-view></gr-settings-view>`); - element.account = createAccountDetailWithId(); - await element.updateComplete; - assert.dom.equal( - queryAndAssert(element, '#allowBrowserNotificationsSection'), - /* HTML */ `<section id="allowBrowserNotificationsSection"> - <div class="title"> - <label for="allowBrowserNotifications"> - Allow browser notifications - </label> - <a - href="/Documentation/user-attention-set.html#_browser_notifications" - target="_blank" - rel="noopener noreferrer" - > - <gr-icon icon="help" title="read documentation"> </gr-icon> - </a> - </div> - <span class="value"> - <input checked="" id="allowBrowserNotifications" type="checkbox" /> - </span> - </section>` - ); - }); - test('calls the title-change event', async () => { const titleChangedStub = sinon.stub(); const newElement = document.createElement('gr-settings-view'); @@ -497,167 +267,6 @@ suite('gr-settings-view tests', () => { assert.equal(titleChangedStub.getCall(0).args[0].detail.title, 'Settings'); }); - test('user preferences', async () => { - // Rendered with the expected preferences selected. - assert.equal( - Number( - ( - valueOf('Changes per page', 'preferences')! - .firstElementChild as GrSelect - ).bindValue - ), - preferences.changes_per_page - ); - assert.equal( - (valueOf('Theme', 'preferences').firstElementChild as GrSelect).bindValue, - preferences.theme - ); - assert.equal( - ( - valueOf('Date/time format', 'preferences')! - .firstElementChild as GrSelect - ).bindValue, - preferences.date_format - ); - assert.equal( - (valueOf('Date/time format', 'preferences')!.lastElementChild as GrSelect) - .bindValue, - preferences.time_format - ); - assert.equal( - ( - valueOf('Email notifications', 'preferences')! - .firstElementChild as GrSelect - ).bindValue, - preferences.email_strategy - ); - assert.equal( - (valueOf('Email format', 'preferences')!.firstElementChild as GrSelect) - .bindValue, - preferences.email_format - ); - assert.equal( - ( - valueOf('Show Relative Dates In Changes Table', 'preferences')! - .firstElementChild as HTMLInputElement - ).checked, - false - ); - assert.equal( - (valueOf('Diff view', 'preferences')!.firstElementChild as GrSelect) - .bindValue, - preferences.diff_view - ); - assert.equal( - ( - valueOf('Show size bars in file list', 'preferences')! - .firstElementChild as HTMLInputElement - ).checked, - true - ); - assert.equal( - ( - valueOf('Publish comments on push', 'preferences')! - .firstElementChild as HTMLInputElement - ).checked, - false - ); - assert.equal( - ( - valueOf( - 'Set new changes to "work in progress" by default', - 'preferences' - )!.firstElementChild as HTMLInputElement - ).checked, - false - ); - assert.equal( - ( - valueOf('Disable token highlighting on hover', 'preferences')! - .firstElementChild as HTMLInputElement - ).checked, - false - ); - assert.equal( - ( - valueOf( - 'Insert Signed-off-by Footer For Inline Edit Changes', - 'preferences' - )!.firstElementChild as HTMLInputElement - ).checked, - false - ); - - assert.isFalse(element.prefsChanged); - - const themeSelect = valueOf('Theme', 'preferences') - .firstElementChild as GrSelect; - themeSelect.bindValue = 'DARK'; - - themeSelect.dispatchEvent( - new CustomEvent('change', { - composed: true, - bubbles: true, - }) - ); - - const publishOnPush = valueOf('Publish comments on push', 'preferences')! - .firstElementChild! as HTMLSpanElement; - - publishOnPush.click(); - - assert.isTrue(element.prefsChanged); - - stubRestApi('savePreferences').callsFake(prefs => { - assertMenusEqual(prefs.my, preferences.my); - assert.equal(prefs.publish_comments_on_push, true); - assert.equal(prefs.theme, AppTheme.DARK); - return Promise.resolve(createDefaultPreferences()); - }); - - // Save the change. - await element.handleSavePreferences(); - assert.isFalse(element.prefsChanged); - }); - - test('publish comments on push', async () => { - const publishCommentsOnPush = valueOf( - 'Publish comments on push', - 'preferences' - )!.firstElementChild! as HTMLSpanElement; - publishCommentsOnPush.click(); - - assert.isTrue(element.prefsChanged); - - stubRestApi('savePreferences').callsFake(prefs => { - assert.equal(prefs.publish_comments_on_push, true); - return Promise.resolve(createDefaultPreferences()); - }); - - // Save the change. - await element.handleSavePreferences(); - assert.isFalse(element.prefsChanged); - }); - - test('set new changes work-in-progress', async () => { - const newChangesWorkInProgress = valueOf( - 'Set new changes to "work in progress" by default', - 'preferences' - )!.firstElementChild! as HTMLSpanElement; - newChangesWorkInProgress.click(); - - assert.isTrue(element.prefsChanged); - - stubRestApi('savePreferences').callsFake(prefs => { - assert.equal(prefs.work_in_progress_by_default, true); - return Promise.resolve(createDefaultPreferences()); - }); - - // Save the change. - await element.handleSavePreferences(); - assert.isFalse(element.prefsChanged); - }); - test('add email validation', async () => { assert.isFalse(element.isNewEmailValid('invalid email')); assert.isTrue(element.isNewEmailValid('vaguely@valid.email')); |