chromium/chrome/browser/resources/chromeos/cloud_upload/cloud_upload_dialog.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import './setup_cancel_dialog.js';

import {assert} from 'chrome://resources/js/assert.js';

import {CANCEL_SETUP_EVENT, NEXT_PAGE_EVENT} from './base_setup_page.js';
import {MetricsRecordedSetupPage, UserAction} from './cloud_upload.mojom-webui.js';
import {CloudUploadBrowserProxy} from './cloud_upload_browser_proxy.js';
import {OfficePwaInstallPageElement} from './office_pwa_install_page.js';
import {OfficeSetupCompletePageElement} from './office_setup_complete_page.js';
import type {SetupCancelDialogElement} from './setup_cancel_dialog.js';
import {SignInPageElement} from './sign_in_page.js';
import {WelcomePageElement} from './welcome_page.js';

/**
 * The CloudUploadElement is the main dialog controller that aggregates all the
 * individual setup pages and determines which one to show.
 */
export class CloudUploadElement extends HTMLElement {
  private proxy = CloudUploadBrowserProxy.getInstance();

  /** Resolved once the element's shadow DOM has finished initializing. */
  initPromise: Promise<void>;

  /** List of pages to show. */
  pages: HTMLElement[] = [];

  /** The current page index into `pages`. */
  private currentPageIdx: number = 0;

  /** The modal dialog shown to confirm if the user wants to cancel setup. */
  private cancelDialog: SetupCancelDialogElement;

  // Save reference to listener so it can be removed from the document in
  // disconnectedCallback().
  private boundKeyDownListener_: (e: KeyboardEvent) => void;

  /**
    True if the setup flow should end with setting Microsoft 365 as default
    handler. Note: This is usually done if no default file handlers have been
    set for Office files, which means that the setup flow is being completed for
    the first time.
  */
  private setOfficeAsDefaultHandler: boolean = true;

  /** The names of the files to upload. */
  private fileNames: string[] = [];

  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});

    this.cancelDialog = document.createElement('setup-cancel-dialog');
    shadow.appendChild(this.cancelDialog);

    this.boundKeyDownListener_ = this.onKeyDown.bind(this);

    this.initPromise = this.init();
  }

  connectedCallback(): void {
    document.addEventListener('keydown', this.boundKeyDownListener_);
  }

  disconnectedCallback(): void {
    document.removeEventListener('keydown', this.boundKeyDownListener_);
  }

  async init(): Promise<void> {
    const [, {installed: isOfficeWebAppInstalled}, {mounted: isOdfsMounted}] =
        await Promise.all([
          this.processDialogArgs(),
          this.proxy.handler.isOfficeWebAppInstalled(),
          this.proxy.handler.isODFSMounted(),
        ]);

    // Only skip this page if the setup flow is not run as part of the "file
    // upload" flow, and file handlers still need to be set.
    if (this.fileNames.length !== 0 || this.setOfficeAsDefaultHandler) {
      const welcomePage = new WelcomePageElement();
      welcomePage.setInstalled(isOfficeWebAppInstalled, isOdfsMounted);
      this.pages.push(welcomePage);
    }

    if (!isOfficeWebAppInstalled) {
      this.pages.push(new OfficePwaInstallPageElement());
    }

    if (!isOdfsMounted) {
      this.pages.push(new SignInPageElement());
    }

    const officeSetupCompletePage = new OfficeSetupCompletePageElement();
    officeSetupCompletePage.setDefaultHandlerOnPageShown(
        this.setOfficeAsDefaultHandler);
    this.pages.push(officeSetupCompletePage);

    for (const page of this.pages) {
      page.addEventListener(NEXT_PAGE_EVENT, () => this.goNextPage());
      page.addEventListener(CANCEL_SETUP_EVENT, () => this.cancelSetup());
    }

    this.switchPage(0);
  }

  $<T extends HTMLElement>(query: string): T {
    return this.shadowRoot!.querySelector(query)!;
  }

  /**
   * Gets the element corresponding to the currently shown page.
   */
  get currentPage(): HTMLElement|undefined {
    return this.pages[this.currentPageIdx];
  }

  /**
   * Switches the currently shown page.
   * @param page Page index to show.
   */
  private switchPage(page: number): void {
    this.currentPage?.remove();
    this.currentPageIdx = page;
    this.shadowRoot!.appendChild(this.currentPage!);
  }

  /**
   * Initialises the class members based off the given dialog arguments.
   */
  private async processDialogArgs(): Promise<void> {
    try {
      const dialogArgs = await this.proxy.handler.getDialogArgs();
      assert(dialogArgs.args);
      assert(dialogArgs.args.dialogSpecificArgs.oneDriveSetupDialogArgs);
      this.setOfficeAsDefaultHandler =
          dialogArgs.args.dialogSpecificArgs.oneDriveSetupDialogArgs
              .setOfficeAsDefaultHandler;
      this.fileNames = dialogArgs.args.fileNames;
    } catch (e) {
      // TODO(b/243095484) Define expected behavior.
      console.error(`Unable to get dialog arguments . Error: ${e}.`);
    }
  }

  private onKeyDown(event: KeyboardEvent) {
    if (event.key === 'Escape' && !this.cancelDialog.open) {
      this.cancelSetup();
      // Stop escape from also immediately closing the dialog.
      event.stopImmediatePropagation();
      event.preventDefault();
    }
  }

  private currentPageToMetricsPage(): MetricsRecordedSetupPage|null {
    if (this.currentPage instanceof WelcomePageElement) {
      return MetricsRecordedSetupPage.kOneDriveSetupWelcome;
    } else if (this.currentPage instanceof OfficePwaInstallPageElement) {
      return MetricsRecordedSetupPage.kOneDriveSetupPWAInstall;
    } else if (this.currentPage instanceof SignInPageElement) {
      return MetricsRecordedSetupPage.kOneDriveSetupODFSMount;
    } else if (this.currentPage instanceof OfficeSetupCompletePageElement) {
      return MetricsRecordedSetupPage.kOneDriveSetupComplete;
    }
    return null;
  }

  /**
   * Invoked when a page fires a `CANCEL_SETUP_EVENT` event.
   */
  private cancelSetup(): void {
    if (this.currentPage instanceof OfficeSetupCompletePageElement) {
      // No need to show the cancel dialog as setup is finished.
      this.proxy.handler.respondWithUserActionAndClose(UserAction.kCancel);
      return;
    }
    this.cancelDialog.show(() => {
      const metricsPage = this.currentPageToMetricsPage();
      if (metricsPage != null) {
        this.proxy.handler.recordCancel(metricsPage);
      }
      this.proxy.handler.respondWithUserActionAndClose(UserAction.kCancel);
    });
  }

  /**
   * Invoked when a page fires a `NEXT_PAGE_EVENT` event.
   */
  private goNextPage(): void {
    if (this.currentPageIdx < this.pages.length - 1) {
      this.switchPage(this.currentPageIdx + 1);
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'cloud-upload': CloudUploadElement;
  }
}

customElements.define('cloud-upload', CloudUploadElement);