chromium/ui/file_manager/file_manager/state/store.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 type {FilesAppEntry} from '../common/js/files_app_entry_types.js';
import type {VolumeType} from '../common/js/volume_manager_types.js';
import {BaseStore} from '../lib/base_store.js';

import {allEntriesSlice} from './ducks/all_entries.js';
import {androidAppsSlice} from './ducks/android_apps.js';
import {bulkPinningSlice} from './ducks/bulk_pinning.js';
import {currentDirectorySlice} from './ducks/current_directory.js';
import {deviceSlice} from './ducks/device.js';
import {driveSlice} from './ducks/drive.js';
import {folderShortcutsSlice} from './ducks/folder_shortcuts.js';
import {launchParamsSlice} from './ducks/launch_params.js';
import {materializedViewsSlice} from './ducks/materialized_views.js';
import {navigationSlice} from './ducks/navigation.js';
import {preferencesSlice} from './ducks/preferences.js';
import {searchSlice} from './ducks/search.js';
import {uiEntriesSlice} from './ducks/ui_entries.js';
import {volumesSlice} from './ducks/volumes.js';
import type {FileData, FileKey, State, Volume} from './state.js';

/**
 * Files app's Store type.
 *
 * It enforces the types for the State and the Actions managed by Files app.
 */
export type Store = BaseStore<State>;

/**
 * Store singleton instance.
 * It's only exposed via `getStore()` to guarantee it's a single instance.
 * TODO(b/272120634): Use window.store temporarily, uncomment below code after
 * the duplicate store issue is resolved.
 */
// let store: null|Store = null;

/**
 * Returns the singleton instance for the Files app's Store.
 *
 * NOTE: This doesn't guarantee the Store's initialization. This should be done
 * at the app's main entry point.
 */
export function getStore(): Store {
  // TODO(b/272120634): Put the store on window to prevent Store being created
  // twice.
  if (!window.store) {
    window.store = new BaseStore<State>(getEmptyState(), [
      searchSlice,
      volumesSlice,
      bulkPinningSlice,
      uiEntriesSlice,
      androidAppsSlice,
      folderShortcutsSlice,
      navigationSlice,
      preferencesSlice,
      deviceSlice,
      driveSlice,
      currentDirectorySlice,
      allEntriesSlice,
      launchParamsSlice,
      materializedViewsSlice,
    ]);
  }

  return window.store;
}

export function getEmptyState(): State {
  // TODO(b/241707820): Migrate State to allow optional attributes.
  return {
    allEntries: {},
    currentDirectory: undefined,
    device: {
      connection: chrome.fileManagerPrivate.DeviceConnectionState.ONLINE,
    },
    drive: {
      connectionType: chrome.fileManagerPrivate.DriveConnectionStateType.ONLINE,
      offlineReason: undefined,
    },
    search: {
      query: undefined,
      status: undefined,
      options: undefined,
    },
    navigation: {
      roots: [],
    },
    volumes: {},
    uiEntries: [],
    folderShortcuts: [],
    androidApps: {},
    bulkPinning: undefined,
    preferences: undefined,
    launchParams: {
      dialogType: undefined,
    },
    materializedViews: [],
  };
}

/**
 * Promise resolved when the store's state in an desired condition.
 *
 * For each Store update the `checker` function is called, when it returns True
 * the promise is resolved.
 *
 * Resolves with the State when it's in the desired condition.
 */
export async function waitForState(
    store: Store, checker: (state: State) => boolean): Promise<State> {
  // Check if the store is already in the desired state.
  if (checker(store.getState())) {
    return store.getState();
  }

  return new Promise((resolve) => {
    const observer = {
      onStateChanged(newState: State) {
        if (checker(newState)) {
          resolve(newState);
          store.unsubscribe(this);
        }
      },
    };
    store.subscribe(observer);
  });
}

/**
 * Returns the `FileData` from a FileKey.
 */
export function getFileData(state: State, key: FileKey): FileData|null {
  const fileData = state.allEntries[key];
  if (fileData) {
    return fileData;
  }
  return null;
}

/**
 * Returns FileData for each key.
 * NOTE: It might return less results than the requested keys when the key isn't
 * found.
 */
export function getFilesData(state: State, keys: FileKey[]): FileData[] {
  const filesData: FileData[] = [];
  for (const key of keys) {
    const fileData = getFileData(state, key);
    if (fileData) {
      filesData.push(fileData);
    }
  }

  return filesData;
}

export function getEntry(state: State, key: FileKey): Entry|FilesAppEntry|null {
  const fileData = state.allEntries[key];
  return fileData?.entry ?? null;
}

export function getVolume(state: State, fileData?: FileData|null): Volume|null {
  const volumeId = fileData?.volumeId;
  return (volumeId && state.volumes[volumeId]) || null;
}

export function getVolumeType(
    state: State, fileData?: FileData|null): VolumeType|null {
  return getVolume(state, fileData)?.volumeType ?? null;
}

/**
 * A function used to select part of the Store.
 * TODO: Can the return type be stronger than `any`?
 */
export type StateSelector = (state: State) => any;