diff --git a/resources/original_messages.json b/resources/original_messages.json index 9bb2656801..fad156b86b 100644 --- a/resources/original_messages.json +++ b/resources/original_messages.json @@ -37,7 +37,7 @@ }, "cancel": { "description": "The text on a button to cancel the current operation.", - "message": "cancel" + "message": "Cancel" }, "change_language_page_title": { "description": "The menu item text and title of a page for a user to set the app language.", @@ -59,6 +59,90 @@ "description": "A status message on a particular server that indicates the application is currently trying to connect to the labeled server.", "message": "Connecting..." }, + "contact_page_title": { + "description": "The menu item text and title of a page for a user to contact the support team.", + "message": "Contact us" + }, + "contact_view_exit_cannot_add_server": { + "description": "Message shown to users who are trying to contact support about an unsupported issue.", + "message": "The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed $START_OF_LINK$here$END_OF_LINK$ and then contact the person who gave you the access key to troubleshoot this issue.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "contact_view_exit_connection": { + "description": "Message shown to users who are trying to contact support about an unsupported issue.", + "message": "The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps listed $START_OF_LINK$hereEND_OF_LINK and then contact the person who gave you the access key to troubleshoot this issue.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "contact_view_exit_no_server": { + "description": "Message shown to users who are trying to contact support about an unsupported issue.", + "message": "The Outline team does not distribute free or paid access keys. $START_OF_LINK$Learn more about how to get an access key.END_OF_LINK", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "contact_view_exit_open_ticket": { + "description": "Message shown to users who are trying to contact support again while having a pending support ticket open.", + "message": "We are currently experiencing high support volume and appreciate your patience. Please do not submit a new request for this concern. If you have additional information to provide, please reply to the initial email about this request." + }, + "contact_view_intro": { + "description": "Introduction message to users who are looking to contact support.", + "message": "Tell us how we can help. Please explain your issue in detail and do not enter personal information that is not requested below." + }, + "contact_view_issue_cannot_add_server": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "I am having trouble adding a server using my access key" + }, + "contact_view_issue_connection": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "I am having trouble connecting to my Outline VPN server" + }, + "contact_view_issue_general": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "General feedback & suggestions" + }, + "contact_view_issue_installation": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "I am having trouble installing Outline" + }, + "contact_view_issue_managing": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "I need assistance managing my Outline VPN server or helping others connect to it" + }, + "contact_view_issue_no_server": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "I need an access key" + }, + "contact_view_issue_performance": { + "description": "Item in a dropdown menu on our contact page to select the issue the user is trying to contact support about.", + "message": "My internet access is slow while connected to my Outline VPN server" + }, + "contact_view_issue": { + "description": "Label of a Contact Support form input field.", + "message": "Outline issue" + }, + "contact_view_open_ticket": { + "description": "Label for a Contact Support form asking if the user already has a pending ticket open with support.", + "message": "Do you have an open ticket for this issue?" + }, "data_collection": { "description": "The text for a main menu item that takes the user to a webpage explaining the application's data collection policies.", "message": "Data collection" @@ -215,6 +299,10 @@ "description": "The menu item text to navigate to the page showing all the software licenses used in building the app.", "message": "Licenses" }, + "no": { + "description": "Negative answer to a form question.", + "message": "No" + }, "non_system_vpn_warning_detail": { "description": "Further explanation message that the user needs to verify that their browser is connected through Outline. Outline is the product name and can be found in the translation glossary.", "message": "Most browsers connect automatically with Outline, some do not." @@ -469,6 +557,50 @@ "description": "Status text showing that a form is progress of being submitted.", "message": "Submitting..." }, + "support_form_access_key_source": { + "description": "Label of a Contact Support form input field.", + "message": "Where did you get your access key?" + }, + "support_form_cloud_provider_aws": { + "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.", + "message": "Amazon Web Services" + }, + "support_form_cloud_provider_digitalocean": { + "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.", + "message": "DigitalOcean" + }, + "support_form_cloud_provider_gcloud": { + "description": "Item in a dropdown menu to select the cloud provider that the user is contacting support about.", + "message": "Google Cloud" + }, + "support_form_cloud_provider_other": { + "description": "Item in a dropdown menu to select an unknown cloud provider that the user is contacting support about.", + "message": "Other" + }, + "support_form_cloud_provider": { + "description": "Label of a Contact Support form input field.", + "message": "Cloud Provider" + }, + "support_form_description": { + "description": "Label of a Contact Support form input field.", + "message": "Description of the issue" + }, + "support_form_email_invalid": { + "description": "Error message shown to a user supplying an invalid email address on the Contact Support form.", + "message": "You have entered an invalid format." + }, + "support_form_email": { + "description": "Label of a Contact Support form input field.", + "message": "Email" + }, + "support_form_required_field": { + "description": "Text on a Contact Support form indicating that a field is required.", + "message": "Required field" + }, + "support_form_subject": { + "description": "Label of a Contact Support form input field.", + "message": "Subject" + }, "terms": { "description": "The text for a main menu item that takes the user to the applications Terms of Service.", "message": "Terms" @@ -502,5 +634,9 @@ "example": "1.0.2" } } + }, + "yes": { + "description": "Affirmative answer to a form question.", + "message": "Yes" } } \ No newline at end of file diff --git a/src/infrastructure/i18n.ts b/src/infrastructure/i18n.ts new file mode 100644 index 0000000000..52011620df --- /dev/null +++ b/src/infrastructure/i18n.ts @@ -0,0 +1,12 @@ +import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat'; + +export type FormattableMessage = + | string + | symbol + | object + | PrimitiveType + | FormatXMLElementFn; + +export interface Localizer { + (messageID: string, ...formatKeyValueList: FormattableMessage[]): string; +} diff --git a/src/www/app/app.ts b/src/www/app/app.ts index baaef9b096..23006b0913 100644 --- a/src/www/app/app.ts +++ b/src/www/app/app.ts @@ -27,6 +27,7 @@ import {Settings, SettingsKey} from './settings'; import {Updater} from './updater'; import {UrlInterceptor} from './url_interceptor'; import {VpnInstaller} from './vpn_installer'; +import {Localizer} from 'src/infrastructure/i18n'; enum OUTLINE_ACCESS_KEY_SCHEME { STATIC = 'ss', @@ -79,7 +80,7 @@ const DEFAULT_SERVER_CONNECTION_STATUS_CHANGE_TIMEOUT = 600; export class App { private feedbackViewEl: polymer.Base; - private localize: (...args: string[]) => string; + private localize: Localizer; private ignoredAccessKeys: {[accessKey: string]: boolean} = {}; private serverConnectionChangeTimeouts: {[serverId: string]: boolean} = {}; diff --git a/src/www/app/main.ts b/src/www/app/main.ts index 18ec3d8308..bd8afb11b4 100644 --- a/src/www/app/main.ts +++ b/src/www/app/main.ts @@ -23,6 +23,7 @@ import {makeConfig, SIP002_URI} from 'ShadowsocksConfig'; import {OutlinePlatform} from './platform'; import {Settings} from './settings'; import {TunnelFactory} from './tunnel'; +import {Localizer} from 'src/infrastructure/i18n.js'; // Used to determine whether to use Polymer functionality on app initialization failure. let webComponentsAreReady = false; @@ -44,7 +45,7 @@ const oncePolymerIsReady = new Promise(resolve => { // Do not call until WebComponentsReady has fired! function getRootEl() { - return (document.querySelector('app-root') as {}) as polymer.Base; + return document.querySelector('app-root') as {} as polymer.Base; } function createServerRepo( @@ -143,7 +144,7 @@ function onUnexpectedError(error: Error) { } // Returns Polymer's localization function. Must be called after WebComponentsReady has fired. -export function getLocalizationFunction() { +export function getLocalizationFunction(): Localizer { const rootEl = getRootEl(); if (!rootEl) { return null; diff --git a/src/www/messages/en.json b/src/www/messages/en.json index 8d174c6a2d..823cf34aff 100644 --- a/src/www/messages/en.json +++ b/src/www/messages/en.json @@ -3,13 +3,27 @@ "about-page-title": "About", "auto-connect-dialog-detail": "Unless you disconnect from your Outline server, Outline will automatically connect next time you restart your device.", "auto-connect-dialog-title": "Stay protected, always", - "cancel": "cancel", + "cancel": "Cancel", "change-language-page-title": "Change Language", "close": "Close", "connect-button-label": "Connect", "connected-server-state": "Connected", "connecting-server-state": "Connecting...", "contact-page-title": "Contact us", + "contact-view-exit-cannot-add-server": "The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed {openLink}here{closeLink} and then contact the person who gave you the access key to troubleshoot this issue.", + "contact-view-exit-connection": "The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps listed {openLink}here{closeLink} and then contact the person who gave you the access key to troubleshoot this issue.", + "contact-view-exit-no-server": "The Outline team does not distribute free or paid access keys. {openLink}Learn more about how to get an access key.{closeLink}", + "contact-view-exit-open-ticket": "We are currently experiencing high support volume and appreciate your patience. Please do not submit a new request for this concern. If you have additional information to provide, please reply to the initial email about this request.", + "contact-view-intro": "Tell us how we can help. Please explain your issue in detail and do not enter personal information that is not requested below.", + "contact-view-issue-cannot-add-server": "I am having trouble adding a server using my access key", + "contact-view-issue-connection": "I am having trouble connecting to my Outline VPN server", + "contact-view-issue-general": "General feedback & suggestions", + "contact-view-issue-installation": "I am having trouble installing Outline", + "contact-view-issue-managing": "I need assistance managing my Outline VPN server or helping others connect to it", + "contact-view-issue-no-server": "I need an access key", + "contact-view-issue-performance": "My internet access is slow while connected to my Outline VPN server", + "contact-view-issue": "Outline issue", + "contact-view-open-ticket": "Do you have an open ticket for this issue?", "data-collection": "Data collection", "disconnect-button-label": "Disconnect", "disconnected-server-state": "Disconnected", @@ -44,6 +58,7 @@ "language-page-title": "Select a language", "learn-more": "Learn More", "licenses-page-title": "Licenses", + "no": "No", "non-system-vpn-warning-detail": "Most browsers connect automatically with Outline, some do not.", "non-system-vpn-warning-title": "Verify your browser connection", "tray-open-window": "Open", @@ -93,11 +108,23 @@ "servers-page-title": "Outline", "submit": "Submit", "submitting": "Submitting...", + "support-form-access-key-source": "Where did you get your access key?", + "support-form-cloud-provider-aws": "Amazon Web Services", + "support-form-cloud-provider-digitalocean": "DigitalOcean", + "support-form-cloud-provider-gcloud": "Google Cloud", + "support-form-cloud-provider-other": "Other", + "support-form-cloud-provider": "Cloud Provider", + "support-form-description": "Description of the issue", + "support-form-email-invalid": "You have entered an invalid format.", + "support-form-email": "Email", + "support-form-required-field": "Required field", + "support-form-subject": "Subject", "terms": "Terms", "tips": "Tips", "undo-button-label": "Undo", "unreachable-server-state": "Unreachable", "unsupported-cipher": "Unsupported cipher", "update-downloaded": "An updated version of Outline has been downloaded. It will be installed when you restart Outline.", - "version": "Version {appVersion}" + "version": "Version {appVersion}", + "yes": "Yes" } diff --git a/src/www/.storybook/localize.ts b/src/www/testing/localize.ts similarity index 71% rename from src/www/.storybook/localize.ts rename to src/www/testing/localize.ts index ad96da8336..00a736a8c0 100644 --- a/src/www/.storybook/localize.ts +++ b/src/www/testing/localize.ts @@ -14,17 +14,11 @@ limitations under the License. */ +import type {FormattableMessage, Localizer} from 'src/infrastructure/i18n'; import englishMessages from '../messages/en.json'; -import IntlMessageFormat, {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat'; +import IntlMessageFormat from 'intl-messageformat'; -type FormattableMessage = - | string - | symbol - | object - | PrimitiveType - | FormatXMLElementFn; - -export const localize = (messageID: string, ...formatKeyValueList: FormattableMessage[]): string => { +export const localize: Localizer = (messageID: string, ...formatKeyValueList: FormattableMessage[]): string => { const message = (englishMessages as {[messageID: string]: string})[messageID]; const formatConfigObject: Record = {}; @@ -38,8 +32,8 @@ export const localize = (messageID: string, ...formatKeyValueList: FormattableMe return `${messageID}(${JSON.stringify(formatConfigObject)})`; } - // we support only english messages in the storybook, for now. - // blocked on modern-web.dev adding support for addons: + // We support only english messages for now. + // Blocked on modern-web.dev adding support for addons: // https://github.com/modernweb-dev/web/issues/1341 return String(new IntlMessageFormat(message, 'en').format(formatConfigObject)); }; diff --git a/src/www/ui_components/app-root.js b/src/www/ui_components/app-root.js index 7f54ce48c1..c465fb11e4 100644 --- a/src/www/ui_components/app-root.js +++ b/src/www/ui_components/app-root.js @@ -298,6 +298,8 @@ export class AppRoot extends mixinBehaviors([AppLocalizeBehavior], PolymerElemen { let el: ContactView; @@ -29,7 +30,7 @@ describe('ContactView', () => { 'SentryErrorReporter', Object.getOwnPropertyNames(SentryErrorReporter.prototype) ); - el = await fixture(html` `); + el = await fixture(html` `); }); it('is defined', async () => { @@ -37,7 +38,7 @@ describe('ContactView', () => { }); it('hides issue selector by default', async () => { - const issueSelector = el.shadowRoot?.querySelector('mwc-select[label="Outline issue"]'); + const issueSelector = el.shadowRoot?.querySelector('mwc-select'); expect(issueSelector?.hasAttribute('hidden')).toBeTrue(); }); @@ -47,7 +48,7 @@ describe('ContactView', () => { }); it('shows exit message if the user selects that they have an open ticket', async () => { - const radioButton = el.shadowRoot!.querySelector('mwc-formfield[label="Yes"] mwc-radio')! as HTMLElement; + const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[0] as HTMLElement; radioButton.click(); await nextFrame(); @@ -56,11 +57,11 @@ describe('ContactView', () => { }); it('shows issue selector if the user selects that they have no open tickets', async () => { - const radioButton = el.shadowRoot!.querySelector('mwc-formfield[label="No"] mwc-radio')! as HTMLElement; + const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[1] as HTMLElement; radioButton.click(); await nextFrame(); - const issueSelector = el.shadowRoot!.querySelector('mwc-select[label="Outline issue"]'); + const issueSelector = el.shadowRoot!.querySelector('mwc-select'); expect(issueSelector?.hasAttribute('hidden')).toBeFalse(); }); @@ -68,8 +69,8 @@ describe('ContactView', () => { let issueSelector: HTMLElement; beforeEach(async () => { - issueSelector = el.shadowRoot!.querySelector('mwc-select[label="Outline issue"]')!; - const radioButton: HTMLElement = el.shadowRoot!.querySelector('mwc-formfield[label="No"] mwc-radio')!; + issueSelector = el.shadowRoot!.querySelector('mwc-select')!; + const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[1] as HTMLElement; radioButton.click(); await nextFrame(); }); diff --git a/src/www/views/contact_view/index.ts b/src/www/views/contact_view/index.ts index 64abbe0f88..c7286f5b56 100644 --- a/src/www/views/contact_view/index.ts +++ b/src/www/views/contact_view/index.ts @@ -17,6 +17,7 @@ import {html, css, LitElement, TemplateResult, nothing} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; import {Ref, createRef, ref} from 'lit/directives/ref.js'; +import {unsafeHTML} from 'lit/directives/unsafe-html.js'; import '@material/mwc-circular-progress'; import '@material/mwc-radio'; import '@material/mwc-select'; @@ -26,10 +27,11 @@ import {SingleSelectedEvent} from '@material/mwc-list/mwc-list'; import './support_form'; import {CardType} from '../shared/card'; -import {IssueType} from './issue_type'; +import {IssueType, UNSUPPORTED_ISSUE_TYPE_HELPPAGES} from './issue_type'; import {AppType} from './app_type'; import {FormValues, SupportForm, ValidFormValues} from './support_form'; import {OutlineErrorReporter} from '../../shared/error_reporter'; +import {Localizer} from 'src/infrastructure/i18n'; /** The possible steps in the stepper. Only one step is shown at a time. */ enum Step { @@ -88,16 +90,9 @@ export class ContactView extends LitElement { `, ]; - private static readonly Issues = new Map([ - [IssueType.INSTALLATION, 'I am having trouble installing Outline'], - [IssueType.NO_SERVER, 'I need an access key'], - [IssueType.ADDING_SERVER, 'I am having trouble adding a server using my access key'], - [IssueType.CONNECTION, 'I am having trouble connecting to my Outline VPN server'], - [IssueType.MANAGING, 'I need assistance managing my Outline VPN server or helping others connect to it'], - [IssueType.INTERNET_SPEED, 'My internet access is slow while connected to my Outline VPN server'], - [IssueType.GENERAL, 'General feedback & suggestions'], - ]); + private static readonly ISSUES = Object.values(IssueType); + @property({type: Function}) localize: Localizer = msg => msg; @property({type: String}) variant: AppType = AppType.CLIENT; @property({type: Object, attribute: 'error-reporter'}) errorReporter: OutlineErrorReporter; @@ -108,17 +103,17 @@ export class ContactView extends LitElement { private readonly openTicketSelectionOptions: Array<{ ref: Ref; value: boolean; - label: string; + labelMsg: string; }> = [ { ref: createRef(), value: true, - label: 'Yes', + labelMsg: 'yes', }, { ref: createRef(), value: false, - label: 'No', + labelMsg: 'no', }, ]; @@ -131,11 +126,7 @@ export class ContactView extends LitElement { const radio = e.target as Radio; const hasOpenTicket = radio.value; if (hasOpenTicket) { - this.exitTemplate = html` - We are currently experiencing high support volume and appreciate your patience. Please do not submit a new - request for this concern. If you have additional information to provide, please reply to the initial email about - this request. - `; + this.exitTemplate = html`${this.localize('contact-view-exit-open-ticket')}`; this.step = Step.EXIT; return; } @@ -146,48 +137,19 @@ export class ContactView extends LitElement { } private selectIssue(e: SingleSelectedEvent) { - this.selectedIssueType = Array.from(ContactView.Issues.keys())[e.detail.index]; - switch (this.selectedIssueType) { - case IssueType.INSTALLATION: - case IssueType.MANAGING: - case IssueType.INTERNET_SPEED: - case IssueType.GENERAL: - this.step = Step.FORM; - break; - case IssueType.NO_SERVER: - // TODO: Send users to localized support pages based on chosen language. - this.exitTemplate = html` - The Outline team does not distribute free or paid access keys. - - Learn more about how to get an access key. - - `; - this.step = Step.EXIT; - break; - case IssueType.ADDING_SERVER: - this.exitTemplate = html` - The Outline team is not able to assist with adding a server. Please try the troubleshooting steps listed - - here - - and then contact the person who gave you the access key to troubleshoot this issue. - `; - this.step = Step.EXIT; - break; - case IssueType.CONNECTION: - this.exitTemplate = html` - The Outline team is not able to assist with connecting to a server. Please try the troubleshooting steps - listed - - here - - and then contact the person who gave you the access key to troubleshoot this issue. - `; - this.step = Step.EXIT; - break; - default: - throw Error('Unexpected issue found'); + this.selectedIssueType = ContactView.ISSUES[e.detail.index]; + + if (UNSUPPORTED_ISSUE_TYPE_HELPPAGES.has(this.selectedIssueType)) { + // TODO: Send users to localized support pages based on chosen language. + this.exitTemplate = this.localizeWithUrl( + `contact-view-exit-${this.selectedIssueType}`, + UNSUPPORTED_ISSUE_TYPE_HELPPAGES.get(this.selectedIssueType) + ); + this.step = Step.EXIT; + return; } + + this.step = Step.FORM; } private reset() { @@ -219,10 +181,15 @@ export class ContactView extends LitElement { this.dispatchEvent(new CustomEvent('success')); } + // TODO: Consider moving this functionality to a more centralized place for re-use. + private localizeWithUrl(messageID: string, url: string): TemplateResult { + const openLink = ``; + const closeLink = ''; + return html` ${unsafeHTML(this.localize(messageID, 'openLink', openLink, 'closeLink', closeLink))} `; + } + private get renderIntroTemplate(): TemplateResult { - return html` -

Tell us how we can help. Please do not enter personal information that is not requested below.

- `; + return html`

${this.localize('contact-view-intro')}

`; } private get renderForm(): TemplateResult | typeof nothing { @@ -234,6 +201,7 @@ export class ContactView extends LitElement { return html` Do you have an open ticket for this issue?

+

${this.localize('contact-view-open-ticket')}

    ${this.openTicketSelectionOptions.map( element => html`
  1. - + - ${Array.from(ContactView.Issues).map(([value, label]) => { + ${ContactView.ISSUES.map(value => { return html` - ${label} + ${this.localize(`contact-view-issue-${value}`)} `; })} diff --git a/src/www/views/contact_view/issue_type.ts b/src/www/views/contact_view/issue_type.ts index 547eb6727b..b1f39af68a 100644 --- a/src/www/views/contact_view/issue_type.ts +++ b/src/www/views/contact_view/issue_type.ts @@ -18,9 +18,16 @@ export enum IssueType { INSTALLATION = 'installation', NO_SERVER = 'no-server', - ADDING_SERVER = 'cannot-add-server', + CANNOT_ADD_SERVER = 'cannot-add-server', CONNECTION = 'connection', MANAGING = 'managing', - INTERNET_SPEED = 'performance', + PERFORMANCE = 'performance', GENERAL = 'general', } + +/** A map of unsupported issue types to helppage URLs to redirect users to. */ +export const UNSUPPORTED_ISSUE_TYPE_HELPPAGES = new Map([ + [IssueType.NO_SERVER, 'https://support.getoutline.org/s/article/How-do-I-get-an-access-key'], + [IssueType.CANNOT_ADD_SERVER, 'https://support.getoutline.org/s/article/What-if-my-access-key-doesn-t-work'], + [IssueType.CONNECTION, 'https://support.getoutline.org/s/article/Why-can-t-I-connect-to-the-Outline-service'], +]); diff --git a/src/www/views/contact_view/stories.ts b/src/www/views/contact_view/stories.ts index 163404a1df..079e6cfe42 100644 --- a/src/www/views/contact_view/stories.ts +++ b/src/www/views/contact_view/stories.ts @@ -20,6 +20,7 @@ import {html} from 'lit'; import './index'; import {AppType} from './app_type'; +import {localize} from '../../testing/localize'; export default { title: 'Contact View', @@ -41,6 +42,7 @@ export default { export const Example = ({variant, onSupportContacted}: {variant: AppType; onSupportContacted: Function}) => html` { it('submit button is disabled by default', async () => { const el = await fixture(html` `); - const submitButton = el.shadowRoot!.querySelector('mwc-button[label="Submit"]')!; + const submitButton = el.shadowRoot!.querySelectorAll('mwc-button')[1] as HTMLElement; expect(submitButton.hasAttribute('disabled')).toBeTrue(); }); @@ -98,7 +98,7 @@ describe('SupportForm', () => { const descriptionInput: TextField = el.shadowRoot!.querySelector('mwc-textarea[name="description"')!; await setValue(descriptionInput, 'Test Description'); - submitButton = el.shadowRoot!.querySelector('mwc-button[label="Submit"]')!; + submitButton = el.shadowRoot!.querySelectorAll('mwc-button')[1] as HTMLElement; }); it('submit button is enabled', async () => { @@ -119,7 +119,7 @@ describe('SupportForm', () => { const el: SupportForm = await fixture(html` `); const listener = oneEvent(el, 'cancel'); - const cancelButton: HTMLElement = el.shadowRoot!.querySelector('mwc-button[label="Cancel"]')!; + const cancelButton = el.shadowRoot!.querySelectorAll('mwc-button')[0] as HTMLElement; cancelButton.click(); const {detail} = await listener; diff --git a/src/www/views/contact_view/support_form/index.ts b/src/www/views/contact_view/support_form/index.ts index 9e83df089d..aea0eb49fc 100644 --- a/src/www/views/contact_view/support_form/index.ts +++ b/src/www/views/contact_view/support_form/index.ts @@ -26,6 +26,7 @@ import {CardType} from '../../shared/card'; import {AppType} from '../app_type'; import {TextField} from '@material/mwc-textfield'; import {SelectedDetail} from '@material/mwc-menu/mwc-menu-base'; +import {Localizer} from 'src/infrastructure/i18n'; type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; @@ -45,6 +46,11 @@ export declare interface ValidFormValues extends FormValues { description: string; } +declare interface CloudProviderOption { + value: string; + label: string; +} + @customElement('support-form') export class SupportForm extends LitElement { static styles = [ @@ -53,6 +59,10 @@ export class SupportForm extends LitElement { font-family: var(--outline-font-family); } + outline-card { + min-width: 100%; + } + mwc-select { width: 100%; } @@ -76,13 +86,9 @@ export class SupportForm extends LitElement { /** The maximum character length of the "Description" field. */ private static readonly MAX_LENGTH_DESCRIPTION = 131072; - private static readonly CLOUD_PROVIDERS = new Map([ - ['aws', 'Amazon Web Services'], - ['digitalocean', 'DigitalOcean'], - ['gcloud', 'Google Cloud'], - ]); - private static readonly OTHER_CLOUD_PROVIDER: [string, string] = ['other', 'Other']; + private static readonly CLOUD_PROVIDERS = ['aws', 'digitalocean', 'gcloud']; + @property({type: Function}) localize: Localizer = msg => msg; @property({type: Boolean}) disabled = false; @property({type: String}) variant: AppType = AppType.CLIENT; @property({type: Object}) values: FormValues = {}; @@ -131,9 +137,11 @@ export class SupportForm extends LitElement { private get renderCloudProviderInputField(): TemplateResult | typeof nothing { if (this.variant !== AppType.MANAGER) return nothing; - const providers = Array.from(SupportForm.CLOUD_PROVIDERS); + const providers = SupportForm.CLOUD_PROVIDERS.map((provider): CloudProviderOption => { + return {value: provider, label: this.localize(`support-form-cloud-provider-${provider}`)}; + }); /** We should sort the providers by their labels, which may be localized. */ - providers.sort(([_valueA, labelA], [_valueB, labelB]) => { + providers.sort(({label: labelA}, {label: labelB}) => { if (labelA < labelB) { return -1; } else if (labelA === labelB) { @@ -142,26 +150,24 @@ export class SupportForm extends LitElement { return 1; } }); - providers.push(SupportForm.OTHER_CLOUD_PROVIDER); + providers.push({value: 'other', label: this.localize('support-form-cloud-provider-other')}); return html` >) => { if (e.detail.index !== -1) { - this.values.cloudProvider = providers[e.detail.index][0]; + this.values.cloudProvider = providers[e.detail.index].value; } }} @blur=${this.checkFormValidity} > - ${providers.map(([value, label]) => html` ${label} `)} + ${providers.map(({value, label}) => html` ${label} `)} `; } @@ -172,9 +178,7 @@ export class SupportForm extends LitElement { return html` -

    * = Required field

    +

    * = ${this.localize('support-form-required-field')}

    - + diff --git a/src/www/views/contact_view/support_form/stories.ts b/src/www/views/contact_view/support_form/stories.ts index f9aec8520c..4cf436d33f 100644 --- a/src/www/views/contact_view/support_form/stories.ts +++ b/src/www/views/contact_view/support_form/stories.ts @@ -21,6 +21,7 @@ import {html} from 'lit'; import './index'; import {AppType} from '../app_type'; import {FormValues} from './index'; +import {localize} from '../../../testing/localize'; export default { title: 'Contact View/Support Form', @@ -56,7 +57,13 @@ export const EmptyForm = ({ onCancel: Function; onSubmit: Function; }) => html` - + `; export const CompleteForm = ({ @@ -79,6 +86,7 @@ export const CompleteForm = ({ }; return html` string; + @property({type: Object}) localize: Localizer; @property({type: Array}) servers: ServerListItem[] = []; @computed('servers') diff --git a/src/www/views/servers_view/server_list/stories.ts b/src/www/views/servers_view/server_list/stories.ts index 9c7bc88e12..6b56c04c96 100644 --- a/src/www/views/servers_view/server_list/stories.ts +++ b/src/www/views/servers_view/server_list/stories.ts @@ -19,7 +19,7 @@ import './index'; import {html} from 'lit'; -import {localize} from '../../../.storybook/localize'; +import {localize} from '../../../testing/localize'; import {ServerList} from './index'; import {ServerConnectionState} from '../server_connection_indicator'; @@ -53,6 +53,4 @@ export default { }; export const Example = ({servers}: ServerList) => - html` - - `; + html` `; diff --git a/src/www/views/servers_view/server_list_item/index.ts b/src/www/views/servers_view/server_list_item/index.ts index 7e1360d8c8..be1086e1ca 100644 --- a/src/www/views/servers_view/server_list_item/index.ts +++ b/src/www/views/servers_view/server_list_item/index.ts @@ -14,6 +14,7 @@ import {Ref} from 'lit/directives/ref'; import {Menu} from '@material/mwc-menu'; import {ServerConnectionState} from '../server_connection_indicator'; +import {Localizer} from 'src/infrastructure/i18n'; export enum ServerListItemEvent { CONNECT = 'ConnectPressed', @@ -41,7 +42,7 @@ export interface ServerListItem { */ export interface ServerListItemElement { server: ServerListItem; - localize: (messageID: string) => string; + localize: Localizer; menu: Ref; menuButton: Ref; } diff --git a/src/www/views/servers_view/server_list_item/server_card/index.ts b/src/www/views/servers_view/server_list_item/server_card/index.ts index 91b1d66cfa..52a218a52f 100644 --- a/src/www/views/servers_view/server_list_item/server_card/index.ts +++ b/src/www/views/servers_view/server_list_item/server_card/index.ts @@ -24,6 +24,7 @@ import {Menu} from '@material/mwc-menu'; import {ServerListItem, ServerListItemElement, ServerListItemEvent} from '..'; import {ServerConnectionState} from '../../server_connection_indicator'; +import {Localizer} from 'src/infrastructure/i18n'; const sharedCSS = css` /* TODO(daniellacosse): reset via postcss */ @@ -190,9 +191,7 @@ const getSharedComponents = (element: ServerListItemElement & LitElement) => { elements: { metadataText: html` `, @@ -231,7 +230,7 @@ const getSharedComponents = (element: ServerListItemElement & LitElement) => { @customElement('server-row-card') export class ServerRowCard extends LitElement implements ServerListItemElement { @property() server: ServerListItem; - @property() localize: (messageID: string) => string; + @property() localize: Localizer; menu: Ref = createRef(); menuButton: Ref = createRef(); @@ -282,7 +281,7 @@ export class ServerRowCard extends LitElement implements ServerListItemElement { @customElement('server-hero-card') export class ServerHeroCard extends LitElement implements ServerListItemElement { @property() server: ServerListItem; - @property() localize: (messageID: string) => string; + @property() localize: Localizer; menu: Ref = createRef(); menuButton: Ref = createRef(); @@ -292,9 +291,9 @@ export class ServerHeroCard extends LitElement implements ServerListItemElement css` .card { --min-indicator-size: 192px; - /* + /* TODO(daniellacosse): calc() in combination with grid in this way can be inconsistent on iOS. - May be resolved by autoprefixer as well. + May be resolved by autoprefixer as well. */ --max-indicator-size: var(--min-indicator-size); @@ -348,9 +347,7 @@ export class ServerHeroCard extends LitElement implements ServerListItemElement return html`
    - + ${elements.menuButton}