mirror of
https://github.com/strapi/strapi.git
synced 2025-08-31 12:23:05 +00:00
future(homepage): add widgets api (#23342)
This commit is contained in:
parent
df1dc7b498
commit
b0ac9d7f6f
@ -1,3 +1,5 @@
|
||||
module.exports = ({ env }) => ({
|
||||
future: {},
|
||||
future: {
|
||||
unstableWidgetsApi: true,
|
||||
},
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ import { CustomFields } from './core/apis/CustomFields';
|
||||
import { Plugin, PluginConfig } from './core/apis/Plugin';
|
||||
import { RBAC, RBACMiddleware } from './core/apis/rbac';
|
||||
import { Router, StrapiAppSetting, UnloadedSettingsLink } from './core/apis/router';
|
||||
import { Widgets } from './core/apis/Widgets';
|
||||
import { RootState, Store, configureStore } from './core/store/configure';
|
||||
import { getBasename } from './core/utils/basename';
|
||||
import { Handler, createHook } from './core/utils/createHook';
|
||||
@ -121,6 +122,7 @@ class StrapiApp {
|
||||
reducers: ReducersMapObject = {};
|
||||
store: Store | null = null;
|
||||
customFields = new CustomFields();
|
||||
widgets = new Widgets();
|
||||
|
||||
constructor({ config, appPlugins }: StrapiAppConstructorArgs = {}) {
|
||||
this.appPlugins = appPlugins || {};
|
||||
|
@ -1,12 +1,17 @@
|
||||
/* eslint-disable check-file/filename-naming-convention */
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { Internal, Utils } from '@strapi/types';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||
import type { AnySchema } from 'yup';
|
||||
|
||||
type CustomFieldUID = `plugin::${string}.${string}` | `global::${string}`;
|
||||
type CustomFieldUID = Utils.String.Suffix<
|
||||
| Internal.Namespace.WithSeparator<Internal.Namespace.Plugin>
|
||||
| Internal.Namespace.WithSeparator<Internal.Namespace.Global>,
|
||||
string
|
||||
>;
|
||||
|
||||
type CustomFieldOptionInput =
|
||||
| 'text'
|
||||
|
64
packages/core/admin/admin/src/core/apis/Widgets.ts
Normal file
64
packages/core/admin/admin/src/core/apis/Widgets.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable check-file/filename-naming-convention */
|
||||
|
||||
import invariant from 'invariant';
|
||||
import { To } from 'react-router-dom';
|
||||
|
||||
import { Permission } from '../../../../shared/contracts/shared';
|
||||
|
||||
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: React.ComponentType;
|
||||
title: MessageDescriptor;
|
||||
link?: {
|
||||
label: MessageDescriptor;
|
||||
href: To;
|
||||
};
|
||||
component: () => Promise<{ default: React.ComponentType }>;
|
||||
pluginId?: string;
|
||||
id: string;
|
||||
permissions?: Permission[];
|
||||
};
|
||||
|
||||
type Widget = Omit<WidgetArgs, 'id' | 'pluginId'> & { uid: WidgetUID };
|
||||
|
||||
class Widgets {
|
||||
widgets: Record<string, Widget>;
|
||||
|
||||
constructor() {
|
||||
this.widgets = {};
|
||||
}
|
||||
|
||||
register = (widget: WidgetArgs | WidgetArgs[]) => {
|
||||
if (Array.isArray(widget)) {
|
||||
widget.forEach((newWidget) => {
|
||||
this.register(newWidget);
|
||||
});
|
||||
} else {
|
||||
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');
|
||||
|
||||
// Replace id and pluginId with computed uid
|
||||
const { id, pluginId, ...widgetToStore } = widget;
|
||||
const uid: WidgetUID = pluginId ? `plugin::${pluginId}.${id}` : `global::${id}`;
|
||||
|
||||
this.widgets[uid] = { ...widgetToStore, uid };
|
||||
}
|
||||
};
|
||||
|
||||
getAll = () => {
|
||||
return Object.values(this.widgets);
|
||||
};
|
||||
}
|
||||
|
||||
export { Widgets };
|
||||
export type { WidgetArgs };
|
102
packages/core/admin/admin/src/core/apis/tests/widgets.test.ts
Normal file
102
packages/core/admin/admin/src/core/apis/tests/widgets.test.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Widgets, type WidgetArgs } from '../Widgets';
|
||||
|
||||
describe('Widgets', () => {
|
||||
let widgets: Widgets;
|
||||
|
||||
beforeEach(() => {
|
||||
widgets = new Widgets();
|
||||
});
|
||||
|
||||
const mockWidget: WidgetArgs = {
|
||||
id: 'test-widget',
|
||||
component: () => Promise.resolve({ default: () => null }),
|
||||
title: { id: 'test.title', defaultMessage: 'Test Title' },
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
describe('register', () => {
|
||||
test('registers a single widget correctly', () => {
|
||||
widgets.register(mockWidget);
|
||||
const registeredWidgets = widgets.getAll();
|
||||
|
||||
expect(registeredWidgets).toHaveLength(1);
|
||||
expect(registeredWidgets[0]).toEqual({
|
||||
component: mockWidget.component,
|
||||
title: mockWidget.title,
|
||||
icon: mockWidget.icon,
|
||||
uid: `global::${mockWidget.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('registers a plugin widget correctly', () => {
|
||||
const pluginWidget = { ...mockWidget, pluginId: 'test-plugin' };
|
||||
widgets.register(pluginWidget);
|
||||
const registeredWidgets = widgets.getAll();
|
||||
|
||||
expect(registeredWidgets).toHaveLength(1);
|
||||
expect(registeredWidgets[0]).toEqual({
|
||||
component: pluginWidget.component,
|
||||
title: pluginWidget.title,
|
||||
icon: pluginWidget.icon,
|
||||
uid: `plugin::${pluginWidget.pluginId}.${pluginWidget.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('registers multiple widgets', () => {
|
||||
const secondWidget = {
|
||||
...mockWidget,
|
||||
id: 'second-widget',
|
||||
};
|
||||
widgets.register([mockWidget, secondWidget]);
|
||||
const registeredWidgets = widgets.getAll();
|
||||
|
||||
expect(registeredWidgets).toHaveLength(2);
|
||||
expect(registeredWidgets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ uid: `global::${mockWidget.id}` }),
|
||||
expect.objectContaining({ uid: `global::${secondWidget.id}` }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when id is missing', () => {
|
||||
const invalidWidget = { ...mockWidget, id: undefined };
|
||||
expect(() => widgets.register(invalidWidget as any)).toThrow('An id must be provided');
|
||||
});
|
||||
|
||||
test('throws when component is missing', () => {
|
||||
const invalidWidget = { ...mockWidget, component: undefined };
|
||||
expect(() => widgets.register(invalidWidget as any)).toThrow('A component must be provided');
|
||||
});
|
||||
|
||||
test('throws when title is missing', () => {
|
||||
const invalidWidget = { ...mockWidget, title: undefined };
|
||||
expect(() => widgets.register(invalidWidget as any)).toThrow('A title must be provided');
|
||||
});
|
||||
|
||||
test('throws when icon is missing', () => {
|
||||
const invalidWidget = { ...mockWidget, icon: undefined };
|
||||
expect(() => widgets.register(invalidWidget as any)).toThrow('An icon must be provided');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
test('returns empty array when no widgets registered', () => {
|
||||
expect(widgets.getAll()).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns all registered widgets as an array', () => {
|
||||
widgets.register(mockWidget);
|
||||
const registeredWidgets = widgets.getAll();
|
||||
|
||||
expect(Array.isArray(registeredWidgets)).toBe(true);
|
||||
expect(registeredWidgets).toHaveLength(1);
|
||||
expect(registeredWidgets[0]).toEqual({
|
||||
component: mockWidget.component,
|
||||
title: mockWidget.title,
|
||||
icon: mockWidget.icon,
|
||||
uid: `global::${mockWidget.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,7 @@
|
||||
export interface FeaturesConfig {
|
||||
future?: object;
|
||||
future?: {
|
||||
unstableWidgetsApi?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FeaturesService {
|
||||
|
Loading…
x
Reference in New Issue
Block a user