From 7a413cac98442b75a20a5ff1ebd17f737ab3a4ba Mon Sep 17 00:00:00 2001
From: Sander Bruens
Date: Fri, 26 Apr 2024 11:29:24 -0400
Subject: [PATCH] feat(manager): add new contact form (#1996)
* Remove manager support from client feedback form.
* Move issue_type exports to index.ts
* Add manager form.
* Some style changes.
* Send feedback to Sentry.
* Don't break the existing feedback flow.
* Fix gallery app.
* Fix submit button string.
* Fix lint issues.
* Update the installation failure flow.
* Address review comments.
* Fix missing cordova plugin in package-lock.
* Fix enum reference.
---
client/src/www/ui_components/app-root.js | 1 -
client/src/www/views/contact_view/app_type.ts | 21 -
.../src/www/views/contact_view/index.spec.ts | 40 +-
client/src/www/views/contact_view/index.ts | 66 +-
.../src/www/views/contact_view/issue_type.ts | 32 -
client/src/www/views/contact_view/stories.ts | 13 +-
.../contact_view/support_form/index.spec.ts | 14 -
.../views/contact_view/support_form/index.ts | 86 +-
.../contact_view/support_form/stories.ts | 17 -
package-lock.json | 2218 ++++++++++++++++-
server_manager/messages/master_messages.json | 105 +
server_manager/package.json | 9 +
server_manager/web_app/app.ts | 2 +-
server_manager/web_app/gallery_app/main.ts | 12 +-
.../web_app/ui_components/app-root.ts | 36 +-
.../outline-contact-us-dialog.ts | 373 +++
.../ui_components/outline-support-form.ts | 216 ++
17 files changed, 3014 insertions(+), 247 deletions(-)
delete mode 100644 client/src/www/views/contact_view/app_type.ts
delete mode 100644 client/src/www/views/contact_view/issue_type.ts
create mode 100644 server_manager/web_app/ui_components/outline-contact-us-dialog.ts
create mode 100644 server_manager/web_app/ui_components/outline-support-form.ts
diff --git a/client/src/www/ui_components/app-root.js b/client/src/www/ui_components/app-root.js
index dae3997229..d16ea06a55 100644
--- a/client/src/www/ui_components/app-root.js
+++ b/client/src/www/ui_components/app-root.js
@@ -352,7 +352,6 @@ export class AppRoot extends mixinBehaviors([AppLocalizeBehavior], PolymerElemen
name="contact"
id="contactView"
localize="[[localize]]"
- variant="client"
error-reporter="[[errorReporter]]"
on-success="showContactSuccessToast"
on-error="showContactErrorToast"
diff --git a/client/src/www/views/contact_view/app_type.ts b/client/src/www/views/contact_view/app_type.ts
deleted file mode 100644
index 7329dd21fb..0000000000
--- a/client/src/www/views/contact_view/app_type.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Copyright 2023 The Outline Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** The app variant to use, which dictates the options available to the user. */
-export enum AppType {
- CLIENT = 'client',
- MANAGER = 'manager',
-}
diff --git a/client/src/www/views/contact_view/index.spec.ts b/client/src/www/views/contact_view/index.spec.ts
index 387f72b55a..95217c6361 100644
--- a/client/src/www/views/contact_view/index.spec.ts
+++ b/client/src/www/views/contact_view/index.spec.ts
@@ -25,7 +25,7 @@ import {OutlineErrorReporter, SentryErrorReporter} from '../../shared/error_repo
import {localize} from '../../testing/localize';
-describe('ContactView client variant', () => {
+describe('ContactView', () => {
let el: ContactView;
let mockErrorReporter: jasmine.SpyObj;
@@ -35,7 +35,7 @@ describe('ContactView client variant', () => {
Object.getOwnPropertyNames(SentryErrorReporter.prototype)
);
el = await fixture(
- html` `
+ html` `
);
});
@@ -198,39 +198,3 @@ describe('ContactView client variant', () => {
});
});
});
-
-describe('ContactView manager variant', () => {
- let el: ContactView;
-
- describe('when the user selects that they have no open tickets', () => {
- let issueSelector: Select;
-
- beforeEach(async () => {
- const mockErrorReporter: jasmine.SpyObj = jasmine.createSpyObj(
- 'SentryErrorReporter',
- Object.getOwnPropertyNames(SentryErrorReporter.prototype)
- );
- el = await fixture(
- html`
-
- `
- );
-
- const radioButton = el.shadowRoot!.querySelectorAll('mwc-formfield mwc-radio')[1] as HTMLElement;
- radioButton.click();
- await nextFrame();
-
- issueSelector = el.shadowRoot!.querySelector('mwc-select')!;
- });
-
- it('shows the issue selector', () => {
- expect(issueSelector.hasAttribute('hidden')).toBeFalse();
- });
-
- it('shows the correct items in the selector', () => {
- const issueItemEls = issueSelector.querySelectorAll('mwc-list-item');
- const issueTypes = Array.from(issueItemEls).map((el: ListItemBase) => el.value);
- expect(issueTypes).toEqual(['cannot-add-server', 'connection', 'managing', 'general']);
- });
- });
-});
diff --git a/client/src/www/views/contact_view/index.ts b/client/src/www/views/contact_view/index.ts
index d287b611cd..9db36c0e54 100644
--- a/client/src/www/views/contact_view/index.ts
+++ b/client/src/www/views/contact_view/index.ts
@@ -29,18 +29,32 @@ import {Ref, createRef, ref} from 'lit/directives/ref.js';
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import './support_form';
-import {AppType} from './app_type';
-import {IssueType, UNSUPPORTED_ISSUE_TYPE_HELPPAGES} from './issue_type';
import {FormValues, SupportForm, ValidFormValues} from './support_form';
import {OutlineErrorReporter} from '../../shared/error_reporter';
/** The possible steps in the stepper. Only one step is shown at a time. */
-enum Step {
+enum ProgressStep {
ISSUE_WIZARD, // Step to ask for their specific issue.
FORM, // The contact form.
EXIT, // Final message to show, if any.
}
+/** Supported issue types in the feedback flow. */
+enum IssueType {
+ NO_SERVER = 'no-server',
+ CANNOT_ADD_SERVER = 'cannot-add-server',
+ CONNECTION = 'connection',
+ PERFORMANCE = 'performance',
+ GENERAL = 'general',
+}
+
+/** A map of unsupported issue types to helppage URLs to redirect users to. */
+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'],
+]);
+
@customElement('contact-view')
export class ContactView extends LitElement {
static styles = [
@@ -67,11 +81,6 @@ export class ContactView extends LitElement {
transform: translate(-50%, -50%);
}
- h1 {
- font-size: 1rem;
- margin-bottom: var(--contact-view-gutter, var(--outline-gutter));
- }
-
p {
margin-top: .25rem;
}
@@ -123,22 +132,18 @@ export class ContactView extends LitElement {
`,
];
- private static readonly ISSUES: {[key in AppType]: IssueType[]} = {
- [AppType.CLIENT]: [
- IssueType.NO_SERVER,
- IssueType.CANNOT_ADD_SERVER,
- IssueType.CONNECTION,
- IssueType.PERFORMANCE,
- IssueType.GENERAL,
- ],
- [AppType.MANAGER]: [IssueType.CANNOT_ADD_SERVER, IssueType.CONNECTION, IssueType.MANAGING, IssueType.GENERAL],
- };
+ private static readonly ISSUES: IssueType[] = [
+ IssueType.NO_SERVER,
+ IssueType.CANNOT_ADD_SERVER,
+ IssueType.CONNECTION,
+ IssueType.PERFORMANCE,
+ IssueType.GENERAL,
+ ];
@property({type: Function}) localize: Localizer = msg => msg;
- @property({type: String}) variant: AppType = AppType.CLIENT;
@property({type: Object, attribute: 'error-reporter'}) errorReporter: OutlineErrorReporter;
- @state() private step: Step = Step.ISSUE_WIZARD;
+ @state() private currentStep: ProgressStep = ProgressStep.ISSUE_WIZARD;
private selectedIssueType?: IssueType;
private exitTemplate?: TemplateResult;
@@ -169,14 +174,14 @@ export class ContactView extends LitElement {
const hasOpenTicket = radio.value;
if (hasOpenTicket) {
this.exitTemplate = html`${this.localize('contact-view-exit-open-ticket')}`;
- this.step = Step.EXIT;
+ this.currentStep = ProgressStep.EXIT;
return;
}
this.showIssueSelector = true;
}
private selectIssue(e: SingleSelectedEvent) {
- this.selectedIssueType = ContactView.ISSUES[this.variant][e.detail.index];
+ 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.
@@ -184,11 +189,11 @@ export class ContactView extends LitElement {
`contact-view-exit-${this.selectedIssueType}`,
UNSUPPORTED_ISSUE_TYPE_HELPPAGES.get(this.selectedIssueType)
);
- this.step = Step.EXIT;
+ this.currentStep = ProgressStep.EXIT;
return;
}
- this.step = Step.FORM;
+ this.currentStep = ProgressStep.FORM;
}
reset() {
@@ -198,7 +203,7 @@ export class ContactView extends LitElement {
if (!element.ref.value) return;
element.ref.value.checked = false;
});
- this.step = Step.ISSUE_WIZARD;
+ this.currentStep = ProgressStep.ISSUE_WIZARD;
this.formValues = {};
}
@@ -248,7 +253,6 @@ export class ContactView extends LitElement {
${this.exitTemplate}
`;
}
- case Step.ISSUE_WIZARD:
+ case ProgressStep.ISSUE_WIZARD:
default: {
return html`
${this.renderIntroTemplate}
@@ -299,7 +303,7 @@ export class ContactView extends LitElement {
?fixedMenuPosition=${true}
@selected="${this.selectIssue}"
>
- ${ContactView.ISSUES[this.variant].map(value => {
+ ${ContactView.ISSUES.map(value => {
return html`
${this.localize(`contact-view-issue-${value}`)}
diff --git a/client/src/www/views/contact_view/issue_type.ts b/client/src/www/views/contact_view/issue_type.ts
deleted file mode 100644
index 19281d6500..0000000000
--- a/client/src/www/views/contact_view/issue_type.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright 2023 The Outline Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** Supported issue types in the feedback flow. */
-export enum IssueType {
- NO_SERVER = 'no-server',
- CANNOT_ADD_SERVER = 'cannot-add-server',
- CONNECTION = 'connection',
- MANAGING = 'managing',
- 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/client/src/www/views/contact_view/stories.ts b/client/src/www/views/contact_view/stories.ts
index 2a33a58fab..5eaac173d6 100644
--- a/client/src/www/views/contact_view/stories.ts
+++ b/client/src/www/views/contact_view/stories.ts
@@ -19,32 +19,21 @@
import {html} from 'lit';
import './index';
-import {AppType} from './app_type';
import {localize} from '../../testing/localize';
export default {
title: 'Contact View',
component: 'contact-view',
argTypes: {
- variant: {
- description: 'Style variant of the contact view.',
- defaultValue: AppType.CLIENT,
- options: Object.values(AppType),
- control: {
- type: 'radio',
- defaultValue: AppType.CLIENT,
- },
- },
onSuccess: {action: 'success'},
onError: {action: 'error'},
},
};
-export const Example = ({variant, onSuccess, onError}: {variant: AppType; onSuccess: Function; onError: Function}) =>
+export const Example = ({onSuccess, onError}: {onSuccess: Function; onError: Function}) =>
html`
{
expect(el).toBeInstanceOf(SupportForm);
});
- it('shows correct fields for the client variant', async () => {
- const el = await fixture(html` `);
-
- expect(el.shadowRoot!.querySelector('mwc-textfield[name="accessKeySource"]')).not.toBeNull();
- expect(el.shadowRoot!.querySelector('mwc-select[name="cloudProvider"]')).toBeNull();
- });
-
- it('shows correct fields for the manager variant', async () => {
- const el = await fixture(html` `);
-
- expect(el.shadowRoot!.querySelector('mwc-textfield[name="accessKeySource"]')).toBeNull();
- expect(el.shadowRoot!.querySelector('mwc-select[name="cloudProvider"]')).not.toBeNull();
- });
-
it('sets fields with provided form values', async () => {
const values: FormValues = {
email: 'foo@bar.com',
diff --git a/client/src/www/views/contact_view/support_form/index.ts b/client/src/www/views/contact_view/support_form/index.ts
index e80a5ff448..959e6e7970 100644
--- a/client/src/www/views/contact_view/support_form/index.ts
+++ b/client/src/www/views/contact_view/support_form/index.ts
@@ -14,21 +14,18 @@
* limitations under the License.
*/
-import {SelectedDetail} from '@material/mwc-menu/mwc-menu-base';
import {TextField} from '@material/mwc-textfield';
+
import '@material/mwc-button';
import '@material/mwc-select';
import '@material/mwc-textarea';
import '@material/mwc-textfield';
-
import {Localizer} from '@outline/infrastructure/i18n';
-import {html, css, LitElement, TemplateResult, nothing, PropertyValues} from 'lit';
+import {html, css, LitElement, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {live} from 'lit/directives/live.js';
import {createRef, Ref, ref} from 'lit/directives/ref.js';
-import {AppType} from '../app_type';
-
type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
@@ -38,7 +35,6 @@ export declare interface FormValues {
subject?: string;
description?: string;
accessKeySource?: string;
- cloudProvider?: string;
}
/** Interface for valid form data. */
@@ -46,11 +42,7 @@ export declare interface ValidFormValues extends FormValues {
email: string;
subject: string;
description: string;
-}
-
-declare interface CloudProviderOption {
- value: string;
- label: string;
+ accessKeySource: string;
}
@customElement('support-form')
@@ -89,12 +81,11 @@ export class SupportForm extends LitElement {
private static readonly DEFAULT_MAX_LENGTH_INPUT = 225;
/** The maximum character length of the "Description" field. */
private static readonly MAX_LENGTH_DESCRIPTION = 131072;
-
- private static readonly CLOUD_PROVIDERS = ['aws', 'digitalocean', 'gcloud'];
+ /** The number of visible text lines for the "Description" field. */
+ private static readonly MAX_ROWS_DESCRIPTION = 5;
@property({type: Function}) localize: Localizer = msg => msg;
@property({type: Boolean}) disabled = false;
- @property({type: String}) variant: AppType = AppType.CLIENT;
@property({type: Object}) values: FormValues = {};
private readonly formRef: Ref = createRef();
@@ -138,60 +129,6 @@ export class SupportForm extends LitElement {
this.checkFormValidity();
}
- private get renderCloudProviderInputField(): TemplateResult | typeof nothing {
- if (this.variant !== AppType.MANAGER) return nothing;
-
- 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(({label: labelA}, {label: labelB}) => {
- if (labelA < labelB) {
- return -1;
- } else if (labelA === labelB) {
- return 0;
- } else {
- return 1;
- }
- });
- 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].value;
- }
- }}
- @blur=${this.checkFormValidity}
- >
- ${providers.map(({value, label}) => html` ${label} `)}
-
- `;
- }
-
- private get renderAccessKeySourceInputField(): TemplateResult | typeof nothing {
- if (this.variant !== AppType.CLIENT) return nothing;
-
- return html`
-
- `;
- }
-
render() {
return html`