Repository: strapi/strapi
File: packages/core/admin/admin/src/core/apis/Widgets.ts (TypeScript)

Code before:

import type { Internal, Utils } from '@strapi/types';
import type { MessageDescriptor } from 'react-intl';

type WidgetUID = Utils.String.Suffix<
  | Internal.Namespace.WithSeparator<Internal.Namespace.Plugin>
  | Internal.Namespace.WithSeparator<Internal.Namespace.Global>,
  string
>;

type WidgetArgs = {
  icon?: typeof import('@strapi/icons').PuzzlePiece;
  title: MessageDescriptor;
  link?: {
    label: MessageDescriptor;
    href: To;
  };
  component: () => Promise<React.ComponentType>;
  pluginId?: string;
  id: string;
  permissions?: Array<Pick<Permission, 'action'> & Partial<Omit<Permission, 'action'>>>;
};

type WidgetWithUID = Omit<WidgetArgs, 'id' | 'pluginId'> & { uid: WidgetUID };

type DescriptionReducer<T extends object> = (prev: T[]) => T[];

class Widgets {
  widgets: WidgetArgs[];

  constructor() {
    this.widgets = [];
  }

  private generateUid = (widget: WidgetArgs): WidgetUID => {
    // At this point, widget must be WidgetArgs (has id and pluginId)
    const widgetArgs = widget as WidgetArgs;
    return (
      widgetArgs.pluginId
        ? `plugin::${widgetArgs.pluginId}.${widgetArgs.id}`
        : `global::${widgetArgs.id}`
    ) as WidgetUID;
  };

  private checkWidgets = (widgets: WidgetArgs[]): void => {
    widgets.forEach((widget) => {
      invariant(widget.id, 'An id must be provided');
      invariant(widget.component, 'A component must be provided');
      invariant(widget.title, 'A title must be provided');
      invariant(widget.icon, 'An icon must be provided');
    });

Review comment (suggestion):
I feel like the reducer type could just be `(prev: WidgetArgs) => WidgetArgs[]`, I don't see why we would need a generic

Code after:

import type { Internal, Utils } from '@strapi/types';
import type { MessageDescriptor } from 'react-intl';

type WidgetUID = Utils.String.Suffix<
  | Internal.Namespace.WithSeparator<Internal.Namespace.Plugin>
  | Internal.Namespace.WithSeparator<Internal.Namespace.Global>,
  string
>;

type WidgetArgs = {
  icon?: typeof import('@strapi/icons').PuzzlePiece;
  title: MessageDescriptor;
  link?: {
    label: MessageDescriptor;
    href: To;
  };
  component: () => Promise<React.ComponentType>;
  pluginId?: string;
  id: string;
  permissions?: Array<Pick<Permission, 'action'> & Partial<Omit<Permission, 'action'>>>;
};

type WidgetWithUID = Omit<WidgetArgs, 'id' | 'pluginId'> & { uid: WidgetUID };

type DescriptionReducer = (prev: WidgetArgs[]) => WidgetArgs[];

class Widgets {
  widgets: WidgetArgs[];

  constructor() {
    this.widgets = [];
  }

  private generateUid = (widget: WidgetArgs): WidgetUID => {
    return widget.pluginId ? `plugin::${widget.pluginId}.${widget.id}` : `global::${widget.id}`;
  };

  private checkWidgets = (widgets: WidgetArgs[]): void => {
    widgets.forEach((widget) => {
      invariant(widget.id, 'An id must be provided');
      invariant(widget.component, 'A component must be provided');
      invariant(widget.title, 'A title must be provided');
      invariant(widget.icon, 'An icon must be provided');
    });
  };

  register(widgets: WidgetArgs | WidgetArgs[] | DescriptionReducer) {
    if (Array.isArray(widgets)) {
      this.checkWidgets(widgets);
      this.widgets = [...this.widgets, ...widgets];