Panel for ListView Command Set (SharePoint Online)
Tags: sharepointonline, spfx, react, typescript
Published: 2022-01-13 16:00:41

> Update 07.02.2023: It's been a moment since I made this sample, and I see it still has readers =) Thank you! 
> I improved it in the meantime (see [GitHub](https://github.com/kkazala/spfx-Panel/)), and I am updating this post accordingly.
> Important: The [new version](https://github.com/kkazala/spfx-Panel/) is now using `SPFx 1.16`

Although SharePoint Online displays list forms in a panel, all the examples and samples for ListView Command Sets are using dialog controls.

Typically, it's a no-brainer to open/close panel if it's controlled by a parent component. In the case of ListView Command Set, this requires slightly more effort. 

![Panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kkffam20rrc3oqopxxj.gif)
## StatefulPanel 

The `StatefulPanel` component is using `isOpen` state to control Panel state. Once it's dismissed, the `isOpen` is set to `false`, and a `props.onDismiss()` method is executed. This is important if you need to refresh the page, after the StatefulPanel is closed. 

To make sure the StatefulPanel may be used the same way a regular Panel is, the type of the props is wrapped in  `React.PropsWithChildren`. 

##### IStatefulPanelProps.ts
```ts
import { IPanelProps } from "@fluentui/react";

export interface IStatefulPanelProps extends IPanelProps {
	panelTop: number;
}
```

##### StatefulPanel.tsx
```ts
import { IPanelStyles, Panel, PanelType } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { loadStyles } from "@microsoft/load-themed-styles";
import * as React from "react";
import { IStatefulPanelProps } from "./IStatefulPanelProps";

export default function StatefulPanel(props: React.PropsWithChildren<IStatefulPanelProps>): JSX.Element {
	const IframePanelStyles: Partial<IPanelStyles> = {
		root: { top: props.panelTop },
	};
	const [isOpen, { setTrue: setPanelOpen, setFalse: setPanelClosed }] = useBoolean(false);

	React.useEffect(() => {
		loadStyles("panel");
		setPanelOpen();
	}, []);

	const _onPanelClosed = (): void => {
		setPanelClosed();

		if (props.onDismiss !== undefined) {
			props.onDismiss();
		}
	};

	return (
		<Panel className="od-Panel" headerText={props.title} isOpen={isOpen} type={PanelType.medium} isLightDismiss={false} styles={IframePanelStyles} onDismiss={_onPanelClosed}>
			{/* Ensure there are children to render, otherwise ErrorBoundary throws error */}
			{props.children && <>{props.children}</>}
		</Panel>
	);
}
```
## Custom component 
MyComponent wraps all the Forms/Lists/Controls in the StatefulPanel.  

##### MyComponent.tsx
```ts
export interface IMyComponentProps { 
    selectedRows: readonly RowAccessor[];
    context: ListViewCommandSetContext;
    panelConfig: IStatefulPanelProps;
}

export default function MyComponent(props: IMyComponentProps) {
    const [refreshPage, setRefreshPage] = useBoolean(false);

    const _onPanelClosed = () => {
        if (refreshPage) {
            //Reloads the entire page since there isn't currently a way to just reload the list view
            location.reload(); 
        }
    };
    
    return <StatefulPanel
        title={props.panelConfig.title}
        panelTop={props.panelConfig.panelTop}
        shouldOpen={props.panelConfig.shouldOpen}
        onDismiss={_onPanelClosed}
    >
        <Toggle
            label="Refresh the page when panel closes:"
            inlineLabel
            onChange={setRefreshPage.toggle}
            onText="Yes"
            offText="No"
            defaultChecked={refreshPage} />
    </StatefulPanel>;
}
```
## CommandSet 
The CommandSet code requires minimal changes to replace Dialog with a Panel.
##### PanelCommandSet.ts
```ts
export default class PanelCommandSet extends BaseListViewCommandSet<IPanelCommandSetProperties> {
  private panelPlaceHolder: HTMLDivElement = null;
  private panelTop: number;

  private _showComponent = (props: IMyComponentProps): void => { 
    ReactDOM.render(React.createElement(MyComponent, props), this.panelPlaceHolder);
  }

  @override
  public onInit(): Promise<void> {
    this.panelTop = document.querySelector("#SuiteNavWrapper").clientHeight;
    this.panelPlaceHolder = document.body.appendChild(document.createElement("div"));
    return Promise.resolve();
  }

  @override
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {

    switch (event.itemId) {
      case 'COMMAND_1':
        this._showComponent({
          panelConfig: {
            panelTop:this.panelTop,
            shouldOpen:true,
            title: "Panel",
          },
          selectedRows: event.selectedRows,
          context: this.context,
        });
        break;
      default:
        throw new Error('Unknown command');
    }
  }
}
```
## Application Insights 

The new version of [SPFx Panel](https://github.com/kkazala/spfx-Panel) is using [ApplicationInsights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/javascript) and a [React plug-in for Application Insights JavaScript SDK](https://learn.microsoft.com/en-us/azure/azure-monitor/app/javascript-react-plugin).

One important thing to remember when using the [React plug-in](https://learn.microsoft.com/en-us/azure/azure-monitor/app/javascript-react-plugin) is that you need to set `allowSyntheticDefaultImports` to `true` in your `tsconfig.json` file:
##### tsconfig.json
```json
{
  "extends": "./node_modules/@microsoft/rush-stack-compiler-4.5/includes/tsconfig-web.json",
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
  }
}
```

## @fluentui/react
The `@microsoft/office-ui-fabric-react-bundle` package is now deprecated and since SPFx 1.16, it's recommended to use `@fluentui/react` instead.

If, just like me, you were already using `@fluentui/react` in your project, make sure that you update the package version to `v8`. Only `@fluentui/react v8` supports `React v17.0.1` (which is [required](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/compatibility#spfx-development-environment-compatibility) for SPFx 1.16)

## Source code
You can see the source code here: https://github.com/kkazala/spfx-Panel
Or, if you prefer to try it our right away, the packaged solution is [here](https://github.com/kkazala/spfx-Panel/blob/main/spfx-panel.sppkg).

UPDATE 17.01.2022: I just created a [suggestion](https://feedbackportal.microsoft.com/feedback/idea/21073578-8777-ec11-a81b-0022484f5e0c) to update @microsoft/generator-sharepoint, to create Panel instead of Dialog. Please vote it up if you agree =)