summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorpaladox <thomasmulhall410@yahoo.com>2024-04-23 15:24:07 +0100
committerPaladox none <thomasmulhall410@yahoo.com>2024-04-24 11:36:23 +0000
commitc981357c32be97724651604bcb30650a3ee6e51c (patch)
tree3b7730cf4ede5f0cb5420a0c74c5b1ee59a26b3e
parent5491f3d6582d6cb9345ccbd9c108889288404331 (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)
-rw-r--r--polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts598
-rw-r--r--polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts454
-rw-r--r--polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts600
-rw-r--r--polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts395
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'));