# Items Experience

The Items Experience is a collection of UI components that interact with Assortment's Items API. Using these components in multiple frontend projects will ensure a consistent user experience across SPS applications.

> "Earthshaking"
>
> -- <cite>@omnipitous</cite>

## Contents

- [Installation](#installation)
- [Basic Setup](#basic-setup)
  - [Context Provider](#context-provider)
  - [API Provider Setup](#api-provider-setup)
- [Routing](#routing)
  - [React Router](#react-router)
  - [Using Your Own Router](#using-your-own-router)
- [Contributing](#contributing)
  - [Installation](#installation-1)
  - [Project Structure](#project-structure)
    - [`lib`](#lib)
    - [`src`](#src)
  - [Testing](#testing)
    - [Unit Tests](#unit-tests)
    - [End-to-end Tests](#end-to-end-tests)
  - [Storybook](#storybook)
  - [Pull Requests and Deployment](#pull-requests-and-deployment)
    - [Pull Requests](#pull-requests)
    - [Deployment](#deployment)
    - [Rotating NPM keys](#rotating-npm-keys)
  - [Scripts](#scripts)

## Installation

Install the package:

```bash
pnpm add @spscommerce/items-experience
```

The package is hosted publicly on the NPM registry, so no extra configuration or permissions are required for installing.

This package requires some Peer Dependencies that should also be installed (refer to `package.json` for versions):

- `@sps-woodland/content-row`
- `@sps-woodland/core`
- `@sps-woodland/file-upload`
- `@sps-woodland/illustrations`
- `@sps-woodland/key-value-set`
- `@sps-woodland/page-title`
- `@sps-woodland/product-bar`
- `@sps-woodland/tokens`
- `@sps-woodland/zero-state`
- `@spscommerce/ds-react`
- `@spscommerce/asst-api`
- `@spscommerce/form-presets-api`
- `@spscommerce/items-api`
- `@spscommerce/policies-api`
- `@spscommerce/rules-management-api`
- `react`
- `react-dom`
- `react-query`
- `react-router-dom` (optional)

## Basic Setup

Items Experience was built to be used with React Router (version 6.4 or later), but is compatible with any routing setup.

All routing options require some initial basic setup, which includes:

- Setting up a context provider
- Setting up API providers (either via the `apiConfig` prop or the `updateApiProviders` function)

Routing can be setup after the basic setup.

### Context Provider

All components from Items Experience must exist within the context of `ItemsExperienceBasicContextProvider`.

`ItemsExperienceBasicContextProvider` provides common data and functions from the host app:

```ts
type ItemsExperienceBasicContextProps = {
  /**
   * `growl` from `@sps-woodland/growler`
   */
  growl: typeof growl;
  /**
   * API configuration options.
   * If not provided, `updateApiProviders` must be called before using Items Experience.
   * If provided, these props will override any previous calls to `updateApiProviders`.
   */
  apiConfig?: {
    /**
     * Either "local", "test", "prod", or an object with the following properties:
     * - asst: "local" | "test" | "prod"
     * - attrreg: "local" | "test" | "prod"
     * - formPresets: "test" | "prod"
     * - identity: "test" | "prod"
     * - items: "local" | "test" | "prod"
     * - policy: "local" | "test" | "prod"
     * - relationships: "local" | "test" | "prod"
     * - rulesMgmt: "local" | "test" | "prod"
     * - workbench: "local" | "test" | "prod"
     *
     * If a string is provided, then all envs will be set to the same value.
     * When local is provided, the formPresets and identity envs will be set to "test".
     */
    env: "local" | "test" | "prod" | EnvConfig;
    /**
     * Token to be used for API requests.
     */
    token: string;
    /**
     * Optional application environment indicator. Can be 'prod' or 'preprod'.
     */
    appEnvironment?: AppEnvironment;
  };
  /**
   * Allows form-presets to be saved for your application
   */
  formPresetDetails: {
    appName: string;
    advancedSearchDefaultsFormName: string;
    selectedColumnsFormName: string;
  };
  /**
   * Options for advanced search.  Currently only includes standard attributes.
   */
  advancedSearchOptions: {
    /**
     * An array of standard attribute Ids that will be Pre-selected attributes in advanced search.
     */
    standardAttributes: string[];
  };
  /**
   * Optional array of available attributes. When provided, attribute select options will be filtered to this list
   */
  availableAttributeList?: string[];
  /**
   * Optional array of item types to be used in searches
   */
  availableItemTypes?: ItemType[];
  /**
   * Config options for the Items Index page
   */
  itemsIndexConfig: {
    /**
     * An array of unique attributes.
     * Affects available columns.
     */
    uniqueAttributes: string[];
    /**
     * Boolean flag to enable Stage Item feature.
     * Affects column chooser.
     */
    isStageItemEnabled: boolean;
    /**
     * Boolean flag to enable MSIS feature.
     * Affects column chooser.
     */
    isMsisUiEnabled: boolean;
    /**
     * Boolean flag to enable Media Item feature.
     */
    enableMediaItem: boolean;
    /**
     * Array of columns that are initially selected.
     */
    columnsInitiallySelected: { value: string; required?: boolean }[];
    /**
     * Array of columns that are initially unselected.
     */
    columnsInitiallyUnselected: { value: string }[];
    /**
     * Optional boolean to hide Status column.
     * Defaults to false.
     */
    hideStatusColumn?: boolean;
  };
  /**
   * An optional {@link QueryClient} can be passed to Items Experience.
   *
   * Note: The {@link QueryCache} will be replaced.
   *
   * It is not recommended to pass a `QueryClient` to Items Experience unless greater control is required, such as disabling retries during testing.
   * Items Experience will create its own `QueryClient`.
   */
  queryClient?: QueryClient;
  /**
   * Optional parameter for passing identity org id allowing the user to act as part of this organization.
   */
  ownerOrgId?: string;
  /**
   * Optional for Item search. Allows filtering items by the requesting application. eg. Assortment or Item XREF.
   */
  applicationType?: ApplicationType;
  /**
   * Optional features configuration to enable/disable specific functionality.
   */
  features?: Partial<ItemsExperienceFeatures>;
  /**
   * Your application, including Items Experience pages
   */
  children: React.ReactNode;
};
```

### API Provider Setup

Items Experience requires a valid user token to make network requests. All page components will wait until a valid token has been provided before rendering to ensure there are no race conditions.

There are two ways to provide API configuration to Items Experience:

#### Option 1: Using the apiConfig prop

You can pass API configuration directly to `ItemsExperienceBasicContextProvider` using the `apiConfig` prop. This approach automatically handles the setup:

```ts
import { ItemsExperienceBasicContextProvider } from "@spscommerce/items-experience";

function App() {
  return (
    <ItemsExperienceBasicContextProvider
      apiConfig={{
        env: "test", // or "local", "prod", or a custom EnvConfig object
        token: "user_token",
        appEnvironment: "preprod", // optional: "prod" or "preprod"
      }}
      // ... other props
    >
      {/* Your app content */}
    </ItemsExperienceBasicContextProvider>
  );
}
```

#### Option 2: Using updateApiProviders function

Alternatively, you can use the `updateApiProviders` function to set up the API configuration separately. This is useful when you need more control over when the API providers are updated:

```ts
import { updateApiProviders } from "@spscommerce/items-experience";

updateApiProviders(env, "user_token", "preprod");
```

**Note:** If you provide both `apiConfig` prop and call `updateApiProviders`, the `apiConfig` prop will override any previous calls to `updateApiProviders`.

### Features Configuration

Items Experience supports feature flags to enable or disable specific functionality. You can configure these features using the optional `features` prop:

```ts
import { ItemsExperienceBasicContextProvider } from "@spscommerce/items-experience";

function App() {
  return (
    <ItemsExperienceBasicContextProvider
      features={{
        items: {
          requiresExportLocaleControl: true,
          requiresProductTypeExports: false,
          requiresItemUomConversionRules: true,
          requiresAddItemButton: true,
          requiresCatalogSelectionForItemExport: false,
          requiresSavedSearchFeature: true,
        },
      }}
      // ... other props
    >
      {/* Your app content */}
    </ItemsExperienceBasicContextProvider>
  );
}
```

Available feature flags:

- `requiresExportLocaleControl`: Enables location control in ExportCatalogModal
- `requiresProductTypeExports`: Enables Product Type Exports in ExportCatalogModal
- `requiresItemUomConversionRules`: Enables Item UOM Conversion Rule Features in ItemIndexPage
- `requiresAddItemButton`: Controls whether the "Add Item" button is shown on ItemIndexPage
- `requiresCatalogSelectionForItemExport`: Enables catalog selection for item export
- `requiresSavedSearchFeature`: Enables saved search feature on ItemIndexPage

If not provided, sensible defaults will be used for all features.

### Draft Notification Setup

The forms in this package save drafts to mitigate the loss of unsaved work done by the end user.
Although optional, it is recommended that the user is notified when a draft is saved.
To do this, Items Experience exports `triggerDraftNotifications()` which must be called after every rout change.

If you are using React-Router, setup may look like this:

```ts
import { useLocation } from "react-router-dom";
import { triggerDraftNotifications } from "@spscommerce/items-experience";

export function Component() {
  // Get the current location from react-router
  const location = useLocation();

  // Call `triggerDraftNotifications` any time `location` changes
  useEffect(() => {
    triggerDraftNotifications();
  }, [location]);

  return ...
}
```

## Routing

Items Experience consists of multiple pages, and therefore requires a routing solution.
Simple routing configuration can be done with React Router, but Items Experience can also be configured to work with any router.

### React Router

If your application uses React Router version 6.4, then setup can be much simpler. Items Experience exports a `RouteObject` that can be added to your app's existing `RouteObject` at any level. It uses relative routing to automatically configure navigation between pages.

`ItemsExperienceBasicContextProvider` and `updateApiProviders` are still required for setup, but `ItemsExperienceUrlContextProvider` is not because routing is handled automatically.

```ts
import { itemsExperienceRouteObject } from "@spscommerce/items-experience/react-router";

const router = createHashRouter([
  {
    path: "/",
    element: <Navigate to="a" />,
  },
  {
    path: "/a",
    element: <A />,
  },
  {
    path: "/b",
    element: <B />,
  },
  {
    path: "/items",
    element: <Outlet />,
    children: [itemsExperienceRouteObject()],
  },
]);
```

Optional parameters can be provided to `itemsExperienceRouteObject`, which allow Items Experience to link to external pages:

```ts
{
  errorsIndexLocation?: string;
  errorsByItemLocation?: string;
  imageSettingsLocation?: string;
  exportsConfigLocation?: string;
}
```

Please see `src/App.tsx` for a full example.

### Using Your Own Router

This package exports each page as a single component so they can be used in your project's routing system. Each page component can exist under the URL of your choice within your project, but those URLs must be provided to `ItemsExperienceUrlContextProps` to enable navigation between pages.

`ItemsExperienceUrlContextProvider` provides routing data and functions from the host app. This component allows Items Experience components to integrate with your application's router:

```ts
type ItemsExperienceUrlContextProviderProps = {
  /**
   * Your application, including Items Experience pages
   */
  children: React.ReactNode;
  /**
   * The current URL pathname (e.g. "/items", "/imports").
   * Optional for backwards compatibility — host apps should provide this
   * so that items-experience can detect which page the user is on
   * (e.g. to redirect from item details when switching environments).
   */
  pathname?: string;
  /**
   * Path parameters for the current route
   */
  urlParams: Record<string, string | undefined>;
  /**
   * Search parameters for the current route
   */
  urlSearchParams: URLSearchParams;
  /**
   * Function to set search parameters. Used for url state.
   * @param params Search parameters to set
   * @param replace Should the new parameters replace the current parameters
   */
  setUrlSearchParams: (params: URLSearchParams, replace: boolean) => void;
  /**
   * A function to navigate to different URLs.
   * @param location The location to navigate to
   */
  navigateTo: (location: string) => void;
  /** Location paths for items-experience pages. */
  locations: {
    /** Errors Index location. External to Items Experience. Optional. */
    errorsIndexLocation?: string;
    /** Error Details location. */
    errorDetailsLocation: string;
    /** Errors by Item location. External to Items Experience. Optional. */
    errorsByItemLocation?: string;
    /** Imports Index location. */
    importsIndexLocation: string;
    /** Imports Create Template location. */
    importsCreateTemplateLocation: string;
    /** Items Index location. */
    itemsIndexLocation: string;
    /** Item Details location. */
    itemDetailsLocation: string;
    /** Advanced Search Defaults location. */
    advSearchDefaultsLocation: string;
    /** Image Settings location. External to Items Experience. Optional. */
    imageSettingsLocation?: string;
    /** Exports Configuration location. External to Items Experience. Optional. */
    exportsConfigLocation?: string;
  };
};
```

> **Note:** The provider automatically derives a `navigate()` function that preserves infrastructure URL params (`executionEnvironment`, `systemInstanceId`) when navigating between items-experience pages. Internal consumers use `useItemsExperienceUrl()` to access `navigate`, `updateSearchParams`, and typed `searchParams`. Host apps only need to provide the raw props above.

Items Experience exports the following page components in each category:

- Preferences
  - `AdvancedSearchDefaultsPage`
- Errors
  - `ErrorDetailsPage`
- Imports
  - `ImportsCreateTemplatePage`
  - `ImportsIndexPage`
- Items
  - `ItemsIndexPage`
  - `ItemDetailsPage`

Some pages expect URL path parameters (`urlParams`) and are required to function as expected:

- `ErrorDetailsPage` expects `itemInfoId`
  - ex. `errors/detail/:itemInfoId`
- `ItemsDetail` expects `id`
  - ex. `items/detail/:id`

When Items Experience pages link to each other, these path parameters will be automatically be appended to the locations provided in `locations`.
The host app needs to provide the path parameters to `ItemsExperienceUrlContextProvider` because Items Experience components do not have direct access to the router.

# Contributing

When contributing, please keep in mind that this is a public package that is used by multiple teams.
Breaking changes need to be documented (see [Pull Requests and Deployment](#pull-requests-and-deployment) for details).

## Installation

This project is part of the Assortment UI workspace and uses `pnpm` for package management.

Install dependencies:

```bash
pnpm i
```

## Project Structure

### `lib`

The `lib` folder contains the code that will be exported. It has two sub-folders: `core` and `react-router`. These sub-folder correspond to the two entrypoints of the package.

- `core` contains the base components the Items Experience, without any routing.

- `react-router` contains the react-router adapter, which is a `RouteObject` that can be dropped into an existing react router config

### `src`

The `src` folder contains an example application that uses the Items Experience with the react-router adapter.

Running the example project:

```bash
pnpm dev
```

## Testing

This project is set up for unit tests and end-to-end tests.

### Unit Tests

This project uses Vitest with React Testing Library for unit tests. These tests will have a `*.test.(ts|tsx)` file extension.

Running tests in watch mode:

```bash
pnpm test
```

Running tests with an explorer UI:

```bash
pnpm test:ui
```

### End-to-end Tests

This project uses Playwright for end-to-end tests. These tests will exist under the `e2e` folder and will have a `*.spec.ts` file extension.

Running tests:

```bash
pnpm test:e2e
```

Playwright also features a UI for step by step debugging.

Running the Playwright UI:

```bash
pnpm test:e2e-ui
```

## Storybook

This project uses Storybook to help with development.

Running Storybook:

```bash
pnpm storybook
```

## Pull Requests and Deployment

This projects uses Microsoft's [beachball](https://microsoft.github.io/beachball/) to manage semantic versioning.

### Pull Requests

When opening a pull request, the build pipeline will check if a changefile is included. The build will fail if no changefile is found. To create a changefile, run the `beachball:change` script. The cli will ask a few questions and then generate a changefile.

When generating a changefile, the type of change must be specified:

- Patch - bug fixes; no backwards incompatible changes.
- Minor - small feature; backwards compatible changes.
- None - this change does not affect the published package in any way.
- Major - major feature; breaking changes.

### Deployment

The project is not automatically deployed when a Pull Request is merged. The project must be deployed manually by running the Items Experience Publish pipeline.

When this pipeline is run, it will run the following on the `main` branch:

- check lint
- check formatting
- run tests
- build
- check for changefiles
- increment package version, delete changefiles, commit to main
- publish the package to NPM

### Rotating NPM keys

The key required to publish the package can only be valid for a maximum of 365 days. When the key expires, it must be replaced with a newly generated key. A newly key can be generated on NPM, using the spsc_asst account.

## Scripts

- `dev`: run the example project
- `test`: run unit tests in watch mode
- `test:ui`: run unit tests with the explorer UI
- `test:ci`: run unit tests in CI
- `test:e2e`: run the playwright e2e tests
- `test:e2e-ui`: run the playwright e2e test explorer and debugger
- `lint:lib`: lint the lib folder
- `lint:src`: lint the src folder
- `prettier:check`: check that all files are formatted correctly
- `storybook`: run storybook
- `build-storybook`: build deployable storybook files (not necessary to run storybook locally)
- `beachball:change`: generate a changefile (at least one changefile is required for a pull request)
- `beachball:check`: check that a changefile accompanies changes that are not in main (used by CI)
- `beachball:publish`: publish the package (used by CI)
